vector-inspector 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. vector_inspector/__init__.py +3 -0
  2. vector_inspector/__main__.py +4 -0
  3. vector_inspector/core/__init__.py +1 -0
  4. vector_inspector/core/connections/__init__.py +7 -0
  5. vector_inspector/core/connections/base_connection.py +233 -0
  6. vector_inspector/core/connections/chroma_connection.py +384 -0
  7. vector_inspector/core/connections/qdrant_connection.py +723 -0
  8. vector_inspector/core/connections/template_connection.py +346 -0
  9. vector_inspector/main.py +21 -0
  10. vector_inspector/services/__init__.py +1 -0
  11. vector_inspector/services/backup_restore_service.py +286 -0
  12. vector_inspector/services/filter_service.py +72 -0
  13. vector_inspector/services/import_export_service.py +287 -0
  14. vector_inspector/services/settings_service.py +60 -0
  15. vector_inspector/services/visualization_service.py +116 -0
  16. vector_inspector/ui/__init__.py +1 -0
  17. vector_inspector/ui/components/__init__.py +1 -0
  18. vector_inspector/ui/components/backup_restore_dialog.py +350 -0
  19. vector_inspector/ui/components/filter_builder.py +370 -0
  20. vector_inspector/ui/components/item_dialog.py +118 -0
  21. vector_inspector/ui/components/loading_dialog.py +30 -0
  22. vector_inspector/ui/main_window.py +288 -0
  23. vector_inspector/ui/views/__init__.py +1 -0
  24. vector_inspector/ui/views/collection_browser.py +112 -0
  25. vector_inspector/ui/views/connection_view.py +423 -0
  26. vector_inspector/ui/views/metadata_view.py +555 -0
  27. vector_inspector/ui/views/search_view.py +268 -0
  28. vector_inspector/ui/views/visualization_view.py +245 -0
  29. vector_inspector-0.2.0.dist-info/METADATA +382 -0
  30. vector_inspector-0.2.0.dist-info/RECORD +32 -0
  31. vector_inspector-0.2.0.dist-info/WHEEL +4 -0
  32. vector_inspector-0.2.0.dist-info/entry_points.txt +5 -0
@@ -0,0 +1,423 @@
1
+ """Connection configuration view."""
2
+
3
+ from PySide6.QtWidgets import (
4
+ QWidget, QVBoxLayout, QHBoxLayout, QLabel,
5
+ QPushButton, QDialog, QFormLayout, QLineEdit,
6
+ QRadioButton, QButtonGroup, QGroupBox, QFileDialog, QComboBox, QApplication, QCheckBox
7
+ )
8
+ from PySide6.QtCore import Signal
9
+
10
+ from vector_inspector.core.connections.base_connection import VectorDBConnection
11
+ from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
12
+ from vector_inspector.core.connections.qdrant_connection import QdrantConnection
13
+ from vector_inspector.ui.components.loading_dialog import LoadingDialog
14
+ from vector_inspector.services.settings_service import SettingsService
15
+
16
+
17
+ class ConnectionDialog(QDialog):
18
+ """Dialog for configuring database connection."""
19
+
20
+ def __init__(self, parent=None):
21
+ super().__init__(parent)
22
+ self.setWindowTitle("Connect to Vector Database")
23
+ self.setMinimumWidth(450)
24
+
25
+ self.settings_service = SettingsService()
26
+
27
+ self.provider = "chromadb"
28
+ self.connection_type = "persistent"
29
+ self.path = ""
30
+ self.host = "localhost"
31
+ self.port = "8000"
32
+
33
+ self._setup_ui()
34
+ self._load_last_connection()
35
+
36
+ def _setup_ui(self):
37
+ """Setup dialog UI."""
38
+ layout = QVBoxLayout(self)
39
+
40
+ # Provider selection
41
+ provider_group = QGroupBox("Database Provider")
42
+ provider_layout = QVBoxLayout()
43
+
44
+ self.provider_combo = QComboBox()
45
+ self.provider_combo.addItem("ChromaDB", "chromadb")
46
+ self.provider_combo.addItem("Qdrant", "qdrant")
47
+ self.provider_combo.currentIndexChanged.connect(self._on_provider_changed)
48
+ provider_layout.addWidget(self.provider_combo)
49
+ provider_group.setLayout(provider_layout)
50
+
51
+ layout.addWidget(provider_group)
52
+
53
+ # Connection type selection
54
+ type_group = QGroupBox("Connection Type")
55
+ type_layout = QVBoxLayout()
56
+
57
+ self.button_group = QButtonGroup()
58
+
59
+ self.persistent_radio = QRadioButton("Persistent (Local File)")
60
+ self.persistent_radio.setChecked(True)
61
+ self.persistent_radio.toggled.connect(self._on_type_changed)
62
+
63
+ self.http_radio = QRadioButton("HTTP (Remote Server)")
64
+
65
+ self.ephemeral_radio = QRadioButton("Ephemeral (In-Memory)")
66
+
67
+ self.button_group.addButton(self.persistent_radio)
68
+ self.button_group.addButton(self.http_radio)
69
+ self.button_group.addButton(self.ephemeral_radio)
70
+
71
+ type_layout.addWidget(self.persistent_radio)
72
+ type_layout.addWidget(self.http_radio)
73
+ type_layout.addWidget(self.ephemeral_radio)
74
+ type_group.setLayout(type_layout)
75
+
76
+ layout.addWidget(type_group)
77
+
78
+ # Connection details
79
+ details_group = QGroupBox("Connection Details")
80
+ form_layout = QFormLayout()
81
+
82
+ # Path input (for persistent) + Browse button
83
+ self.path_input = QLineEdit()
84
+ # Default to user's test data folder
85
+ self.path_input.setText("./data/chrome_db")
86
+ path_row_widget = QWidget()
87
+ path_row_layout = QHBoxLayout(path_row_widget)
88
+ path_row_layout.setContentsMargins(0, 0, 0, 0)
89
+ path_row_layout.addWidget(self.path_input)
90
+ browse_button = QPushButton("Browse…")
91
+ browse_button.clicked.connect(self._browse_for_path)
92
+ path_row_layout.addWidget(browse_button)
93
+ form_layout.addRow("Data Path:", path_row_widget)
94
+
95
+ # Host input (for HTTP)
96
+ self.host_input = QLineEdit()
97
+ self.host_input.setText("localhost")
98
+ self.host_input.setEnabled(False)
99
+ form_layout.addRow("Host:", self.host_input)
100
+
101
+ # Port input (for HTTP)
102
+ self.port_input = QLineEdit()
103
+ self.port_input.setText("8000")
104
+ self.port_input.setEnabled(False)
105
+ form_layout.addRow("Port:", self.port_input)
106
+
107
+ # API Key input (for Qdrant Cloud)
108
+ self.api_key_input = QLineEdit()
109
+ self.api_key_input.setEnabled(False)
110
+ self.api_key_input.setEchoMode(QLineEdit.Password)
111
+ self.api_key_row = form_layout.rowCount()
112
+ form_layout.addRow("API Key:", self.api_key_input)
113
+
114
+ details_group.setLayout(form_layout)
115
+ layout.addWidget(details_group)
116
+
117
+ # Auto-connect option
118
+ self.auto_connect_check = QCheckBox("Auto-connect on startup")
119
+ self.auto_connect_check.setChecked(False)
120
+ layout.addWidget(self.auto_connect_check)
121
+
122
+ # Buttons
123
+ button_layout = QHBoxLayout()
124
+
125
+ connect_button = QPushButton("Connect")
126
+ connect_button.clicked.connect(self.accept)
127
+ connect_button.setDefault(True)
128
+
129
+ cancel_button = QPushButton("Cancel")
130
+ cancel_button.clicked.connect(self.reject)
131
+
132
+ button_layout.addStretch()
133
+ button_layout.addWidget(connect_button)
134
+ button_layout.addWidget(cancel_button)
135
+
136
+ layout.addLayout(button_layout)
137
+
138
+ # Resolved absolute path preview
139
+ self.absolute_path_label = QLabel("")
140
+ self.absolute_path_label.setStyleSheet("color: gray; font-size: 11px;")
141
+ layout.addWidget(self.absolute_path_label)
142
+
143
+ # Update preview when inputs change
144
+ self.path_input.textChanged.connect(self._update_absolute_preview)
145
+ self.persistent_radio.toggled.connect(self._update_absolute_preview)
146
+ self._update_absolute_preview()
147
+
148
+ def _on_provider_changed(self):
149
+ """Handle provider selection change."""
150
+ self.provider = self.provider_combo.currentData()
151
+
152
+ # Update default port based on provider
153
+ if self.provider == "qdrant":
154
+ if self.port_input.text() == "8000":
155
+ self.port_input.setText("6333")
156
+ elif self.provider == "chromadb":
157
+ if self.port_input.text() == "6333":
158
+ self.port_input.setText("8000")
159
+
160
+ # Show/hide API key field
161
+ is_http = self.http_radio.isChecked()
162
+ self.api_key_input.setEnabled(is_http and self.provider == "qdrant")
163
+
164
+ def _on_type_changed(self):
165
+ """Handle connection type change."""
166
+ is_persistent = self.persistent_radio.isChecked()
167
+ is_http = self.http_radio.isChecked()
168
+
169
+ self.path_input.setEnabled(is_persistent)
170
+ self.host_input.setEnabled(is_http)
171
+ self.port_input.setEnabled(is_http)
172
+ self.api_key_input.setEnabled(is_http and self.provider == "qdrant")
173
+
174
+ self._update_absolute_preview()
175
+
176
+ def get_connection_config(self):
177
+ """Get connection configuration from dialog."""
178
+ config = {"provider": self.provider}
179
+
180
+ if self.persistent_radio.isChecked():
181
+ config.update({"type": "persistent", "path": self.path_input.text()})
182
+ elif self.http_radio.isChecked():
183
+ config.update({
184
+ "type": "http",
185
+ "host": self.host_input.text(),
186
+ "port": int(self.port_input.text()),
187
+ "api_key": self.api_key_input.text() if self.api_key_input.text() else None
188
+ })
189
+ else:
190
+ config.update({"type": "ephemeral"})
191
+
192
+ # Save auto-connect preference
193
+ config["auto_connect"] = self.auto_connect_check.isChecked()
194
+
195
+ # Save this configuration for next time
196
+ self.settings_service.save_last_connection(config)
197
+
198
+ return config
199
+
200
+ def _update_absolute_preview(self):
201
+ """Show resolved absolute path for persistent connections."""
202
+ if not self.persistent_radio.isChecked():
203
+ self.absolute_path_label.setText("")
204
+ return
205
+ rel = self.path_input.text().strip() or "."
206
+ # Resolve relative to project root by searching for pyproject.toml
207
+ from pathlib import Path
208
+ current = Path(__file__).resolve()
209
+ abs_path = None
210
+ for parent in current.parents:
211
+ if (parent / "pyproject.toml").exists():
212
+ abs_path = (parent / rel).resolve()
213
+ break
214
+ if abs_path is None:
215
+ abs_path = Path(rel).resolve()
216
+ self.absolute_path_label.setText(f"Resolved path: {abs_path}")
217
+
218
+ def _browse_for_path(self):
219
+ """Open a folder chooser to select persistent storage path."""
220
+ # Suggest current resolved path as starting point
221
+ start_dir = None
222
+ from pathlib import Path
223
+ rel = self.path_input.text().strip() or "."
224
+ current = Path(__file__).resolve()
225
+ for parent in current.parents:
226
+ if (parent / "pyproject.toml").exists():
227
+ start_dir = str((parent / rel).resolve())
228
+ break
229
+ if start_dir is None:
230
+ start_dir = str(Path(rel).resolve())
231
+ directory = QFileDialog.getExistingDirectory(self, "Select ChromaDB Data Directory", start_dir)
232
+ if directory:
233
+ # Set as relative to project root if within it, else absolute
234
+ proj_root = None
235
+ for parent in current.parents:
236
+ if (parent / "pyproject.toml").exists():
237
+ proj_root = parent
238
+ break
239
+ dir_path = Path(directory)
240
+ if proj_root and proj_root in dir_path.parents:
241
+ try:
242
+ display_path = str(dir_path.relative_to(proj_root))
243
+ except Exception:
244
+ display_path = str(dir_path)
245
+ else:
246
+ display_path = str(dir_path)
247
+ self.path_input.setText(display_path)
248
+ self._update_absolute_preview()
249
+
250
+ def _load_last_connection(self):
251
+ """Load and populate the last connection configuration."""
252
+ last_config = self.settings_service.get_last_connection()
253
+ if not last_config:
254
+ return
255
+
256
+ # Set provider
257
+ provider = last_config.get("provider", "chromadb")
258
+ index = self.provider_combo.findData(provider)
259
+ if index >= 0:
260
+ self.provider_combo.setCurrentIndex(index)
261
+
262
+ # Set connection type
263
+ conn_type = last_config.get("type", "persistent")
264
+ if conn_type == "persistent":
265
+ self.persistent_radio.setChecked(True)
266
+ path = last_config.get("path", "")
267
+ if path:
268
+ self.path_input.setText(path)
269
+ elif conn_type == "http":
270
+ self.http_radio.setChecked(True)
271
+ host = last_config.get("host", "localhost")
272
+ port = last_config.get("port", "8000")
273
+ self.host_input.setText(host)
274
+ self.port_input.setText(str(port))
275
+ api_key = last_config.get("api_key")
276
+ if api_key:
277
+ self.api_key_input.setText(api_key)
278
+ elif conn_type == "ephemeral":
279
+ self.ephemeral_radio.setChecked(True)
280
+
281
+ # Set auto-connect checkbox
282
+ auto_connect = last_config.get("auto_connect", False)
283
+ self.auto_connect_check.setChecked(auto_connect)
284
+
285
+
286
+ class ConnectionView(QWidget):
287
+ """Widget for managing database connection."""
288
+
289
+ connection_changed = Signal(bool)
290
+ connection_created = Signal(VectorDBConnection) # Signal when new connection is created
291
+
292
+ def __init__(self, connection: VectorDBConnection, parent=None):
293
+ super().__init__(parent)
294
+ self.connection = connection
295
+ self.loading_dialog = LoadingDialog("Connecting to database...", self)
296
+ self.settings_service = SettingsService()
297
+ self._setup_ui()
298
+
299
+ # Try to auto-connect if enabled in settings
300
+ self._try_auto_connect()
301
+
302
+ def _setup_ui(self):
303
+ """Setup widget UI."""
304
+ layout = QVBoxLayout(self)
305
+ layout.setContentsMargins(0, 0, 0, 0)
306
+
307
+ # Connection status group
308
+ group = QGroupBox("Connection")
309
+ group_layout = QVBoxLayout()
310
+
311
+ self.status_label = QLabel("Status: Not connected")
312
+ group_layout.addWidget(self.status_label)
313
+
314
+ # Button layout with both connect and disconnect
315
+ button_layout = QHBoxLayout()
316
+
317
+ self.connect_button = QPushButton("Connect")
318
+ self.connect_button.clicked.connect(self.show_connection_dialog)
319
+ button_layout.addWidget(self.connect_button)
320
+
321
+ self.disconnect_button = QPushButton("Disconnect")
322
+ self.disconnect_button.clicked.connect(self._disconnect)
323
+ self.disconnect_button.setEnabled(False)
324
+ button_layout.addWidget(self.disconnect_button)
325
+
326
+ group_layout.addLayout(button_layout)
327
+
328
+ group.setLayout(group_layout)
329
+ layout.addWidget(group)
330
+
331
+ def show_connection_dialog(self):
332
+ """Show connection configuration dialog."""
333
+ dialog = ConnectionDialog(self)
334
+
335
+ if dialog.exec() == QDialog.Accepted:
336
+ config = dialog.get_connection_config()
337
+ self._connect_with_config(config)
338
+
339
+ def _connect_with_config(self, config: dict):
340
+ """Connect to database with given configuration."""
341
+ self.loading_dialog.show_loading("Connecting to database...")
342
+ QApplication.processEvents()
343
+
344
+ provider = config.get("provider", "chromadb")
345
+ conn_type = config.get("type")
346
+
347
+ # Create appropriate connection instance based on provider
348
+ if provider == "qdrant":
349
+ if conn_type == "persistent":
350
+ self.connection = QdrantConnection(path=config.get("path"))
351
+ elif conn_type == "http":
352
+ self.connection = QdrantConnection(
353
+ host=config.get("host"),
354
+ port=config.get("port"),
355
+ api_key=config.get("api_key")
356
+ )
357
+ else: # ephemeral/memory
358
+ self.connection = QdrantConnection()
359
+ else: # chromadb
360
+ if conn_type == "persistent":
361
+ self.connection = ChromaDBConnection(path=config.get("path"))
362
+ elif conn_type == "http":
363
+ self.connection = ChromaDBConnection(
364
+ host=config.get("host"),
365
+ port=config.get("port")
366
+ )
367
+ else: # ephemeral
368
+ self.connection = ChromaDBConnection()
369
+
370
+ # Notify parent that connection instance changed
371
+ self.connection_created.emit(self.connection)
372
+ success = self.connection.connect()
373
+
374
+ if success:
375
+ # Show provider, path/host + collection count for clarity
376
+ details = []
377
+ details.append(f"provider: {provider}")
378
+ if hasattr(self.connection, 'path') and self.connection.path:
379
+ details.append(f"path: {self.connection.path}")
380
+ if hasattr(self.connection, 'host') and self.connection.host:
381
+ port = getattr(self.connection, 'port', None)
382
+ details.append(f"host: {self.connection.host}:{port}")
383
+ collections = self.connection.list_collections()
384
+ count_text = f"collections: {len(collections)}"
385
+ info = ", ".join(details)
386
+ self.status_label.setText(f"Status: Connected ({info}, {count_text})")
387
+
388
+ # Enable disconnect, disable connect
389
+ self.connect_button.setEnabled(False)
390
+ self.disconnect_button.setEnabled(True)
391
+
392
+ # Emit signal which triggers collection browser refresh
393
+ self.connection_changed.emit(True)
394
+
395
+ # Process events to ensure collection browser is updated
396
+ QApplication.processEvents()
397
+ else:
398
+ self.status_label.setText("Status: Connection failed")
399
+ # Enable connect, disable disconnect
400
+ self.connect_button.setEnabled(True)
401
+ self.disconnect_button.setEnabled(False)
402
+ self.connection_changed.emit(False)
403
+
404
+ # Close loading dialog after everything is complete
405
+ self.loading_dialog.hide_loading()
406
+
407
+ def _disconnect(self):
408
+ """Disconnect from database."""
409
+ self.connection.disconnect()
410
+ self.status_label.setText("Status: Not connected")
411
+
412
+ # Enable connect, disable disconnect
413
+ self.connect_button.setEnabled(True)
414
+ self.disconnect_button.setEnabled(False)
415
+
416
+ self.connection_changed.emit(False)
417
+
418
+ def _try_auto_connect(self):
419
+ """Try to automatically connect if auto-connect is enabled."""
420
+ last_config = self.settings_service.get_last_connection()
421
+ if last_config and last_config.get("auto_connect", False):
422
+ # Auto-connect is enabled
423
+ self._connect_with_config(last_config)