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.
- vector_inspector/__init__.py +3 -0
- vector_inspector/__main__.py +4 -0
- vector_inspector/core/__init__.py +1 -0
- vector_inspector/core/connections/__init__.py +7 -0
- vector_inspector/core/connections/base_connection.py +233 -0
- vector_inspector/core/connections/chroma_connection.py +384 -0
- vector_inspector/core/connections/qdrant_connection.py +723 -0
- vector_inspector/core/connections/template_connection.py +346 -0
- vector_inspector/main.py +21 -0
- vector_inspector/services/__init__.py +1 -0
- vector_inspector/services/backup_restore_service.py +286 -0
- vector_inspector/services/filter_service.py +72 -0
- vector_inspector/services/import_export_service.py +287 -0
- vector_inspector/services/settings_service.py +60 -0
- vector_inspector/services/visualization_service.py +116 -0
- vector_inspector/ui/__init__.py +1 -0
- vector_inspector/ui/components/__init__.py +1 -0
- vector_inspector/ui/components/backup_restore_dialog.py +350 -0
- vector_inspector/ui/components/filter_builder.py +370 -0
- vector_inspector/ui/components/item_dialog.py +118 -0
- vector_inspector/ui/components/loading_dialog.py +30 -0
- vector_inspector/ui/main_window.py +288 -0
- vector_inspector/ui/views/__init__.py +1 -0
- vector_inspector/ui/views/collection_browser.py +112 -0
- vector_inspector/ui/views/connection_view.py +423 -0
- vector_inspector/ui/views/metadata_view.py +555 -0
- vector_inspector/ui/views/search_view.py +268 -0
- vector_inspector/ui/views/visualization_view.py +245 -0
- vector_inspector-0.2.0.dist-info/METADATA +382 -0
- vector_inspector-0.2.0.dist-info/RECORD +32 -0
- vector_inspector-0.2.0.dist-info/WHEEL +4 -0
- vector_inspector-0.2.0.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Main application window."""
|
|
2
|
+
|
|
3
|
+
from PySide6.QtWidgets import (
|
|
4
|
+
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
5
|
+
QSplitter, QTabWidget, QStatusBar, QToolBar,
|
|
6
|
+
QMessageBox, QInputDialog, QFileDialog
|
|
7
|
+
)
|
|
8
|
+
from PySide6.QtCore import Qt, Signal
|
|
9
|
+
from PySide6.QtGui import QAction
|
|
10
|
+
|
|
11
|
+
from vector_inspector.core.connections.base_connection import VectorDBConnection
|
|
12
|
+
from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
|
|
13
|
+
from vector_inspector.ui.views.connection_view import ConnectionView
|
|
14
|
+
from vector_inspector.ui.views.collection_browser import CollectionBrowser
|
|
15
|
+
from vector_inspector.ui.views.metadata_view import MetadataView
|
|
16
|
+
from vector_inspector.ui.views.search_view import SearchView
|
|
17
|
+
from vector_inspector.ui.views.visualization_view import VisualizationView
|
|
18
|
+
from vector_inspector.ui.components.backup_restore_dialog import BackupRestoreDialog
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MainWindow(QMainWindow):
|
|
22
|
+
"""Main application window with all views and controls."""
|
|
23
|
+
|
|
24
|
+
connection_changed = Signal(bool) # Emits True when connected, False when disconnected
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.connection: VectorDBConnection = ChromaDBConnection()
|
|
29
|
+
self.current_collection: str = ""
|
|
30
|
+
|
|
31
|
+
self.setWindowTitle("Vector Inspector")
|
|
32
|
+
self.setGeometry(100, 100, 1400, 900)
|
|
33
|
+
|
|
34
|
+
self._setup_ui()
|
|
35
|
+
self._setup_menu_bar()
|
|
36
|
+
self._setup_toolbar()
|
|
37
|
+
self._setup_statusbar()
|
|
38
|
+
self._connect_signals()
|
|
39
|
+
|
|
40
|
+
def _setup_ui(self):
|
|
41
|
+
"""Setup the main UI layout."""
|
|
42
|
+
# Central widget with splitter
|
|
43
|
+
central_widget = QWidget()
|
|
44
|
+
self.setCentralWidget(central_widget)
|
|
45
|
+
|
|
46
|
+
layout = QHBoxLayout(central_widget)
|
|
47
|
+
layout.setContentsMargins(5, 5, 5, 5)
|
|
48
|
+
|
|
49
|
+
# Main splitter (left panel | right tabs)
|
|
50
|
+
main_splitter = QSplitter(Qt.Horizontal)
|
|
51
|
+
|
|
52
|
+
# Left panel - Connection and Collections
|
|
53
|
+
left_panel = QWidget()
|
|
54
|
+
left_layout = QVBoxLayout(left_panel)
|
|
55
|
+
left_layout.setContentsMargins(0, 0, 0, 0)
|
|
56
|
+
|
|
57
|
+
self.connection_view = ConnectionView(self.connection)
|
|
58
|
+
self.collection_browser = CollectionBrowser(self.connection)
|
|
59
|
+
|
|
60
|
+
left_layout.addWidget(self.connection_view)
|
|
61
|
+
left_layout.addWidget(self.collection_browser)
|
|
62
|
+
|
|
63
|
+
# Right panel - Tabbed views
|
|
64
|
+
self.tab_widget = QTabWidget()
|
|
65
|
+
|
|
66
|
+
self.metadata_view = MetadataView(self.connection)
|
|
67
|
+
self.search_view = SearchView(self.connection)
|
|
68
|
+
self.visualization_view = VisualizationView(self.connection)
|
|
69
|
+
|
|
70
|
+
self.tab_widget.addTab(self.metadata_view, "Data Browser")
|
|
71
|
+
self.tab_widget.addTab(self.search_view, "Search")
|
|
72
|
+
self.tab_widget.addTab(self.visualization_view, "Visualization")
|
|
73
|
+
|
|
74
|
+
# Add panels to splitter
|
|
75
|
+
main_splitter.addWidget(left_panel)
|
|
76
|
+
main_splitter.addWidget(self.tab_widget)
|
|
77
|
+
main_splitter.setStretchFactor(0, 1)
|
|
78
|
+
main_splitter.setStretchFactor(1, 3)
|
|
79
|
+
|
|
80
|
+
layout.addWidget(main_splitter)
|
|
81
|
+
|
|
82
|
+
def _setup_menu_bar(self):
|
|
83
|
+
"""Setup application menu bar."""
|
|
84
|
+
menubar = self.menuBar()
|
|
85
|
+
|
|
86
|
+
# File menu
|
|
87
|
+
file_menu = menubar.addMenu("&File")
|
|
88
|
+
|
|
89
|
+
connect_action = QAction("&Connect to Database...", self)
|
|
90
|
+
connect_action.setShortcut("Ctrl+O")
|
|
91
|
+
connect_action.triggered.connect(self._on_connect)
|
|
92
|
+
file_menu.addAction(connect_action)
|
|
93
|
+
|
|
94
|
+
disconnect_action = QAction("&Disconnect", self)
|
|
95
|
+
disconnect_action.triggered.connect(self._on_disconnect)
|
|
96
|
+
file_menu.addAction(disconnect_action)
|
|
97
|
+
|
|
98
|
+
file_menu.addSeparator()
|
|
99
|
+
|
|
100
|
+
exit_action = QAction("E&xit", self)
|
|
101
|
+
exit_action.setShortcut("Ctrl+Q")
|
|
102
|
+
exit_action.triggered.connect(self.close)
|
|
103
|
+
file_menu.addAction(exit_action)
|
|
104
|
+
|
|
105
|
+
# Collection menu
|
|
106
|
+
collection_menu = menubar.addMenu("&Collection")
|
|
107
|
+
|
|
108
|
+
new_collection_action = QAction("&New Collection...", self)
|
|
109
|
+
new_collection_action.setShortcut("Ctrl+N")
|
|
110
|
+
new_collection_action.triggered.connect(self._on_new_collection)
|
|
111
|
+
collection_menu.addAction(new_collection_action)
|
|
112
|
+
|
|
113
|
+
refresh_action = QAction("&Refresh Collections", self)
|
|
114
|
+
refresh_action.setShortcut("F5")
|
|
115
|
+
refresh_action.triggered.connect(self._on_refresh_collections)
|
|
116
|
+
collection_menu.addAction(refresh_action)
|
|
117
|
+
|
|
118
|
+
collection_menu.addSeparator()
|
|
119
|
+
|
|
120
|
+
backup_action = QAction("&Backup/Restore...", self)
|
|
121
|
+
backup_action.setShortcut("Ctrl+B")
|
|
122
|
+
backup_action.triggered.connect(self._on_backup_restore)
|
|
123
|
+
collection_menu.addAction(backup_action)
|
|
124
|
+
|
|
125
|
+
# Help menu
|
|
126
|
+
help_menu = menubar.addMenu("&Help")
|
|
127
|
+
|
|
128
|
+
about_action = QAction("&About", self)
|
|
129
|
+
about_action.triggered.connect(self._show_about)
|
|
130
|
+
help_menu.addAction(about_action)
|
|
131
|
+
|
|
132
|
+
def _setup_toolbar(self):
|
|
133
|
+
"""Setup application toolbar."""
|
|
134
|
+
toolbar = QToolBar("Main Toolbar")
|
|
135
|
+
toolbar.setMovable(False)
|
|
136
|
+
self.addToolBar(toolbar)
|
|
137
|
+
|
|
138
|
+
connect_action = QAction("Connect", self)
|
|
139
|
+
connect_action.triggered.connect(self._on_connect)
|
|
140
|
+
toolbar.addAction(connect_action)
|
|
141
|
+
|
|
142
|
+
disconnect_action = QAction("Disconnect", self)
|
|
143
|
+
disconnect_action.triggered.connect(self._on_disconnect)
|
|
144
|
+
toolbar.addAction(disconnect_action)
|
|
145
|
+
toolbar.addSeparator()
|
|
146
|
+
|
|
147
|
+
backup_action = QAction("Backup/Restore", self)
|
|
148
|
+
backup_action.triggered.connect(self._on_backup_restore)
|
|
149
|
+
toolbar.addAction(backup_action)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
toolbar.addSeparator()
|
|
153
|
+
|
|
154
|
+
refresh_action = QAction("Refresh", self)
|
|
155
|
+
refresh_action.triggered.connect(self._on_refresh_collections)
|
|
156
|
+
toolbar.addAction(refresh_action)
|
|
157
|
+
|
|
158
|
+
def _setup_statusbar(self):
|
|
159
|
+
"""Setup status bar."""
|
|
160
|
+
self.statusBar = QStatusBar()
|
|
161
|
+
self.setStatusBar(self.statusBar)
|
|
162
|
+
self.statusBar.showMessage("Not connected")
|
|
163
|
+
|
|
164
|
+
def _connect_signals(self):
|
|
165
|
+
"""Connect signals between components."""
|
|
166
|
+
self.connection_view.connection_changed.connect(self._on_connection_status_changed)
|
|
167
|
+
self.connection_view.connection_created.connect(self._on_connection_created)
|
|
168
|
+
self.collection_browser.collection_selected.connect(self._on_collection_selected)
|
|
169
|
+
|
|
170
|
+
def _on_connection_created(self, new_connection: VectorDBConnection):
|
|
171
|
+
"""Handle when a new connection instance is created."""
|
|
172
|
+
self.connection = new_connection
|
|
173
|
+
# Update all views with new connection
|
|
174
|
+
self.collection_browser.connection = new_connection
|
|
175
|
+
self.metadata_view.connection = new_connection
|
|
176
|
+
self.search_view.connection = new_connection
|
|
177
|
+
self.visualization_view.connection = new_connection
|
|
178
|
+
|
|
179
|
+
def _on_connect(self):
|
|
180
|
+
"""Handle connect action."""
|
|
181
|
+
self.connection_view.show_connection_dialog()
|
|
182
|
+
|
|
183
|
+
def _on_disconnect(self):
|
|
184
|
+
"""Handle disconnect action."""
|
|
185
|
+
if self.connection.is_connected:
|
|
186
|
+
self.connection.disconnect()
|
|
187
|
+
self.statusBar.showMessage("Disconnected")
|
|
188
|
+
self.connection_changed.emit(False)
|
|
189
|
+
self.collection_browser.clear()
|
|
190
|
+
|
|
191
|
+
def _on_connection_status_changed(self, connected: bool):
|
|
192
|
+
"""Handle connection status change."""
|
|
193
|
+
if connected:
|
|
194
|
+
self.statusBar.showMessage("Connected")
|
|
195
|
+
self.connection_changed.emit(True)
|
|
196
|
+
self._on_refresh_collections()
|
|
197
|
+
else:
|
|
198
|
+
self.statusBar.showMessage("Connection failed")
|
|
199
|
+
self.connection_changed.emit(False)
|
|
200
|
+
|
|
201
|
+
def _on_collection_selected(self, collection_name: str):
|
|
202
|
+
"""Handle collection selection."""
|
|
203
|
+
self.current_collection = collection_name
|
|
204
|
+
self.statusBar.showMessage(f"Collection: {collection_name}")
|
|
205
|
+
|
|
206
|
+
# Update all views with new collection
|
|
207
|
+
self.metadata_view.set_collection(collection_name)
|
|
208
|
+
self.search_view.set_collection(collection_name)
|
|
209
|
+
self.visualization_view.set_collection(collection_name)
|
|
210
|
+
|
|
211
|
+
def _on_refresh_collections(self):
|
|
212
|
+
"""Refresh collection list."""
|
|
213
|
+
if self.connection.is_connected:
|
|
214
|
+
self.collection_browser.refresh()
|
|
215
|
+
|
|
216
|
+
def _on_new_collection(self):
|
|
217
|
+
"""Create a new collection."""
|
|
218
|
+
if not self.connection.is_connected:
|
|
219
|
+
QMessageBox.warning(self, "Not Connected", "Please connect to a database first.")
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
|
|
223
|
+
from vector_inspector.core.connections.qdrant_connection import QdrantConnection
|
|
224
|
+
|
|
225
|
+
name, ok = QInputDialog.getText(
|
|
226
|
+
self, "New Collection", "Enter collection name:"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if ok and name:
|
|
230
|
+
success = False
|
|
231
|
+
|
|
232
|
+
# Handle ChromaDB
|
|
233
|
+
if isinstance(self.connection, ChromaDBConnection):
|
|
234
|
+
collection = self.connection.get_collection(name)
|
|
235
|
+
success = collection is not None
|
|
236
|
+
|
|
237
|
+
# Handle Qdrant
|
|
238
|
+
elif isinstance(self.connection, QdrantConnection):
|
|
239
|
+
# Ask for vector size (required for Qdrant)
|
|
240
|
+
vector_size, ok = QInputDialog.getInt(
|
|
241
|
+
self,
|
|
242
|
+
"Vector Size",
|
|
243
|
+
"Enter vector dimension size:",
|
|
244
|
+
value=384, # Default for sentence transformers
|
|
245
|
+
min=1,
|
|
246
|
+
max=10000
|
|
247
|
+
)
|
|
248
|
+
if ok:
|
|
249
|
+
success = self.connection.create_collection(name, vector_size)
|
|
250
|
+
|
|
251
|
+
if success:
|
|
252
|
+
QMessageBox.information(
|
|
253
|
+
self, "Success", f"Collection '{name}' created successfully."
|
|
254
|
+
)
|
|
255
|
+
self._on_refresh_collections()
|
|
256
|
+
else:
|
|
257
|
+
QMessageBox.warning(
|
|
258
|
+
self, "Error", f"Failed to create collection '{name}'."
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
def _on_backup_restore(self):
|
|
262
|
+
"""Open backup/restore dialog."""
|
|
263
|
+
if not self.connection.is_connected:
|
|
264
|
+
QMessageBox.warning(self, "Not Connected", "Please connect to a database first.")
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
dialog = BackupRestoreDialog(
|
|
268
|
+
self.connection,
|
|
269
|
+
self.current_collection,
|
|
270
|
+
self
|
|
271
|
+
)
|
|
272
|
+
dialog.exec()
|
|
273
|
+
|
|
274
|
+
# Refresh collections after dialog closes (in case something was restored)
|
|
275
|
+
self._on_refresh_collections()
|
|
276
|
+
|
|
277
|
+
def _show_about(self):
|
|
278
|
+
"""Show about dialog."""
|
|
279
|
+
QMessageBox.about(
|
|
280
|
+
self,
|
|
281
|
+
"About Vector Inspector",
|
|
282
|
+
"<h2>Vector Inspector 0.1.0</h2>"
|
|
283
|
+
"<p>A comprehensive desktop application for visualizing, "
|
|
284
|
+
"querying, and managing vector database data.</p>"
|
|
285
|
+
'<p><a href="https://github.com/anthonypdawson/vector-viewer" style="color:#2980b9;">GitHub Project Page</a></p>'
|
|
286
|
+
"<hr />"
|
|
287
|
+
"<p>Built with PySide6 and ChromaDB</p>"
|
|
288
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Main application views."""
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Collection browser for listing and selecting collections."""
|
|
2
|
+
|
|
3
|
+
from PySide6.QtWidgets import (
|
|
4
|
+
QWidget, QVBoxLayout, QListWidget, QListWidgetItem,
|
|
5
|
+
QGroupBox, QLabel, QMenu
|
|
6
|
+
)
|
|
7
|
+
from PySide6.QtCore import Signal, Qt
|
|
8
|
+
from PySide6.QtGui import QAction
|
|
9
|
+
|
|
10
|
+
from vector_inspector.core.connections.base_connection import VectorDBConnection
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CollectionBrowser(QWidget):
|
|
14
|
+
"""Widget for browsing and selecting collections."""
|
|
15
|
+
|
|
16
|
+
collection_selected = Signal(str)
|
|
17
|
+
|
|
18
|
+
def __init__(self, connection: VectorDBConnection, parent=None):
|
|
19
|
+
super().__init__(parent)
|
|
20
|
+
self.connection = connection
|
|
21
|
+
self._setup_ui()
|
|
22
|
+
|
|
23
|
+
def _setup_ui(self):
|
|
24
|
+
"""Setup widget UI."""
|
|
25
|
+
layout = QVBoxLayout(self)
|
|
26
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
27
|
+
|
|
28
|
+
group = QGroupBox("Collections")
|
|
29
|
+
group_layout = QVBoxLayout()
|
|
30
|
+
|
|
31
|
+
self.collection_list = QListWidget()
|
|
32
|
+
self.collection_list.itemClicked.connect(self._on_collection_clicked)
|
|
33
|
+
self.collection_list.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
34
|
+
self.collection_list.customContextMenuRequested.connect(self._show_context_menu)
|
|
35
|
+
|
|
36
|
+
group_layout.addWidget(self.collection_list)
|
|
37
|
+
|
|
38
|
+
self.info_label = QLabel("No collections")
|
|
39
|
+
self.info_label.setWordWrap(True)
|
|
40
|
+
self.info_label.setStyleSheet("color: gray; font-size: 10px;")
|
|
41
|
+
group_layout.addWidget(self.info_label)
|
|
42
|
+
|
|
43
|
+
group.setLayout(group_layout)
|
|
44
|
+
layout.addWidget(group)
|
|
45
|
+
|
|
46
|
+
def refresh(self):
|
|
47
|
+
"""Refresh collection list."""
|
|
48
|
+
self.collection_list.clear()
|
|
49
|
+
|
|
50
|
+
if not self.connection.is_connected:
|
|
51
|
+
self.info_label.setText("Not connected")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
collections = self.connection.list_collections()
|
|
55
|
+
|
|
56
|
+
if not collections:
|
|
57
|
+
# Show more context for persistent connections
|
|
58
|
+
if self.connection.path:
|
|
59
|
+
self.info_label.setText(
|
|
60
|
+
f"No collections found at {self.connection.path}"
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
self.info_label.setText("No collections found")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
for collection_name in collections:
|
|
67
|
+
item = QListWidgetItem(collection_name)
|
|
68
|
+
self.collection_list.addItem(item)
|
|
69
|
+
|
|
70
|
+
self.info_label.setText(f"{len(collections)} collection(s)")
|
|
71
|
+
|
|
72
|
+
def clear(self):
|
|
73
|
+
"""Clear collection list."""
|
|
74
|
+
self.collection_list.clear()
|
|
75
|
+
self.info_label.setText("No collections")
|
|
76
|
+
|
|
77
|
+
def _on_collection_clicked(self, item: QListWidgetItem):
|
|
78
|
+
"""Handle collection selection."""
|
|
79
|
+
collection_name = item.text()
|
|
80
|
+
self.collection_selected.emit(collection_name)
|
|
81
|
+
|
|
82
|
+
# Show collection info
|
|
83
|
+
info = self.connection.get_collection_info(collection_name)
|
|
84
|
+
if info:
|
|
85
|
+
count = info.get("count", 0)
|
|
86
|
+
fields = info.get("metadata_fields", [])
|
|
87
|
+
fields_str = ", ".join(fields[:3])
|
|
88
|
+
if len(fields) > 3:
|
|
89
|
+
fields_str += "..."
|
|
90
|
+
self.info_label.setText(
|
|
91
|
+
f"{count} items | Fields: {fields_str if fields else 'None'}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def _show_context_menu(self, position):
|
|
95
|
+
"""Show context menu for collections."""
|
|
96
|
+
item = self.collection_list.itemAt(position)
|
|
97
|
+
if not item:
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
menu = QMenu(self)
|
|
101
|
+
|
|
102
|
+
delete_action = QAction("Delete Collection", self)
|
|
103
|
+
delete_action.triggered.connect(lambda: self._delete_collection(item.text()))
|
|
104
|
+
menu.addAction(delete_action)
|
|
105
|
+
|
|
106
|
+
menu.exec(self.collection_list.mapToGlobal(position))
|
|
107
|
+
|
|
108
|
+
def _delete_collection(self, collection_name: str):
|
|
109
|
+
"""Delete a collection."""
|
|
110
|
+
# TODO: Add confirmation dialog
|
|
111
|
+
if self.connection.delete_collection(collection_name):
|
|
112
|
+
self.refresh()
|