vector-inspector 0.3.3__py3-none-any.whl → 0.3.5__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/core/connections/base_connection.py +86 -1
- vector_inspector/core/connections/chroma_connection.py +23 -3
- vector_inspector/core/connections/pgvector_connection.py +1100 -0
- vector_inspector/core/connections/pinecone_connection.py +24 -4
- vector_inspector/core/connections/qdrant_connection.py +224 -189
- vector_inspector/core/embedding_providers/provider_factory.py +33 -38
- vector_inspector/core/embedding_utils.py +2 -2
- vector_inspector/services/backup_restore_service.py +41 -33
- vector_inspector/ui/components/connection_manager_panel.py +96 -77
- vector_inspector/ui/components/profile_manager_panel.py +315 -121
- vector_inspector/ui/dialogs/embedding_config_dialog.py +79 -58
- vector_inspector/ui/main_window.py +22 -0
- vector_inspector/ui/views/connection_view.py +215 -116
- vector_inspector/ui/views/info_panel.py +6 -6
- vector_inspector/ui/views/metadata_view.py +466 -187
- {vector_inspector-0.3.3.dist-info → vector_inspector-0.3.5.dist-info}/METADATA +9 -2
- {vector_inspector-0.3.3.dist-info → vector_inspector-0.3.5.dist-info}/RECORD +19 -18
- {vector_inspector-0.3.3.dist-info → vector_inspector-0.3.5.dist-info}/WHEEL +0 -0
- {vector_inspector-0.3.3.dist-info → vector_inspector-0.3.5.dist-info}/entry_points.txt +0 -0
|
@@ -2,238 +2,293 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
from PySide6.QtWidgets import (
|
|
5
|
-
QWidget,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
QWidget,
|
|
6
|
+
QVBoxLayout,
|
|
7
|
+
QHBoxLayout,
|
|
8
|
+
QListWidget,
|
|
9
|
+
QListWidgetItem,
|
|
10
|
+
QPushButton,
|
|
11
|
+
QMenu,
|
|
12
|
+
QMessageBox,
|
|
13
|
+
QLabel,
|
|
14
|
+
QDialog,
|
|
15
|
+
QFormLayout,
|
|
16
|
+
QLineEdit,
|
|
17
|
+
QComboBox,
|
|
18
|
+
QRadioButton,
|
|
19
|
+
QButtonGroup,
|
|
20
|
+
QGroupBox,
|
|
21
|
+
QFileDialog,
|
|
22
|
+
QCheckBox,
|
|
23
|
+
QProgressDialog,
|
|
9
24
|
)
|
|
10
|
-
from PySide6.QtCore import Qt, Signal
|
|
25
|
+
from PySide6.QtCore import Qt, Signal, QThread
|
|
11
26
|
|
|
12
27
|
from vector_inspector.services.profile_service import ProfileService, ConnectionProfile
|
|
13
28
|
|
|
14
29
|
|
|
15
30
|
class ProfileManagerPanel(QWidget):
|
|
16
31
|
"""Panel for managing saved connection profiles.
|
|
17
|
-
|
|
32
|
+
|
|
18
33
|
Signals:
|
|
19
34
|
connect_profile: Emitted when user wants to connect to a profile (profile_id)
|
|
20
35
|
"""
|
|
21
|
-
|
|
36
|
+
|
|
22
37
|
connect_profile = Signal(str) # profile_id
|
|
23
|
-
|
|
38
|
+
|
|
24
39
|
def __init__(self, profile_service: ProfileService, parent=None):
|
|
25
40
|
"""
|
|
26
41
|
Initialize profile manager panel.
|
|
27
|
-
|
|
42
|
+
|
|
28
43
|
Args:
|
|
29
44
|
profile_service: The ProfileService instance
|
|
30
45
|
parent: Parent widget
|
|
31
46
|
"""
|
|
32
47
|
super().__init__(parent)
|
|
33
48
|
self.profile_service = profile_service
|
|
34
|
-
|
|
49
|
+
|
|
35
50
|
self._setup_ui()
|
|
36
51
|
self._connect_signals()
|
|
37
52
|
self._refresh_profiles()
|
|
38
|
-
|
|
53
|
+
|
|
39
54
|
def _setup_ui(self):
|
|
40
55
|
"""Setup the UI."""
|
|
41
56
|
layout = QVBoxLayout(self)
|
|
42
57
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
43
|
-
|
|
58
|
+
|
|
44
59
|
# Header
|
|
45
60
|
header_layout = QHBoxLayout()
|
|
46
61
|
header_label = QLabel("Saved Profiles")
|
|
47
62
|
header_label.setStyleSheet("font-weight: bold; font-size: 12px;")
|
|
48
63
|
header_layout.addWidget(header_label)
|
|
49
64
|
header_layout.addStretch()
|
|
50
|
-
|
|
65
|
+
|
|
51
66
|
# New profile button
|
|
52
67
|
self.new_profile_btn = QPushButton("+")
|
|
53
68
|
self.new_profile_btn.setMaximumWidth(30)
|
|
54
69
|
self.new_profile_btn.setToolTip("Create new profile")
|
|
55
70
|
self.new_profile_btn.clicked.connect(self._create_profile)
|
|
56
71
|
header_layout.addWidget(self.new_profile_btn)
|
|
57
|
-
|
|
72
|
+
|
|
58
73
|
layout.addLayout(header_layout)
|
|
59
|
-
|
|
74
|
+
|
|
60
75
|
# Profile list
|
|
61
76
|
self.profile_list = QListWidget()
|
|
62
77
|
self.profile_list.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
63
78
|
self.profile_list.customContextMenuRequested.connect(self._show_context_menu)
|
|
64
79
|
self.profile_list.itemDoubleClicked.connect(self._on_profile_double_clicked)
|
|
65
80
|
layout.addWidget(self.profile_list)
|
|
66
|
-
|
|
81
|
+
|
|
67
82
|
# Action buttons
|
|
68
83
|
button_layout = QHBoxLayout()
|
|
69
|
-
|
|
84
|
+
|
|
70
85
|
self.connect_btn = QPushButton("Connect")
|
|
71
86
|
self.connect_btn.clicked.connect(self._connect_selected_profile)
|
|
72
87
|
button_layout.addWidget(self.connect_btn)
|
|
73
|
-
|
|
88
|
+
|
|
74
89
|
self.edit_btn = QPushButton("Edit")
|
|
75
90
|
self.edit_btn.clicked.connect(self._edit_selected_profile)
|
|
76
91
|
button_layout.addWidget(self.edit_btn)
|
|
77
|
-
|
|
92
|
+
|
|
78
93
|
self.delete_btn = QPushButton("Delete")
|
|
79
94
|
self.delete_btn.clicked.connect(self._delete_selected_profile)
|
|
80
95
|
button_layout.addWidget(self.delete_btn)
|
|
81
|
-
|
|
96
|
+
|
|
82
97
|
layout.addLayout(button_layout)
|
|
83
|
-
|
|
98
|
+
|
|
84
99
|
def _connect_signals(self):
|
|
85
100
|
"""Connect to profile service signals."""
|
|
86
101
|
self.profile_service.profile_added.connect(self._refresh_profiles)
|
|
87
102
|
self.profile_service.profile_updated.connect(self._refresh_profiles)
|
|
88
103
|
self.profile_service.profile_deleted.connect(self._refresh_profiles)
|
|
89
|
-
|
|
104
|
+
|
|
90
105
|
def _refresh_profiles(self):
|
|
91
106
|
"""Refresh the profile list."""
|
|
92
107
|
self.profile_list.clear()
|
|
93
|
-
|
|
108
|
+
|
|
94
109
|
profiles = self.profile_service.get_all_profiles()
|
|
95
110
|
for profile in profiles:
|
|
96
111
|
item = QListWidgetItem(f"{profile.name} ({profile.provider})")
|
|
97
112
|
item.setData(Qt.UserRole, profile.id)
|
|
98
113
|
self.profile_list.addItem(item)
|
|
99
|
-
|
|
114
|
+
|
|
100
115
|
def _on_profile_double_clicked(self, item: QListWidgetItem):
|
|
101
116
|
"""Handle profile double-click to connect."""
|
|
102
117
|
profile_id = item.data(Qt.UserRole)
|
|
103
118
|
if profile_id:
|
|
104
119
|
self.connect_profile.emit(profile_id)
|
|
105
|
-
|
|
120
|
+
|
|
106
121
|
def _connect_selected_profile(self):
|
|
107
122
|
"""Connect to the selected profile."""
|
|
108
123
|
current_item = self.profile_list.currentItem()
|
|
109
124
|
if not current_item:
|
|
110
125
|
QMessageBox.warning(self, "No Selection", "Please select a profile to connect.")
|
|
111
126
|
return
|
|
112
|
-
|
|
127
|
+
|
|
113
128
|
profile_id = current_item.data(Qt.UserRole)
|
|
114
129
|
self.connect_profile.emit(profile_id)
|
|
115
|
-
|
|
130
|
+
|
|
116
131
|
def _edit_selected_profile(self):
|
|
117
132
|
"""Edit the selected profile."""
|
|
118
133
|
current_item = self.profile_list.currentItem()
|
|
119
134
|
if not current_item:
|
|
120
135
|
QMessageBox.warning(self, "No Selection", "Please select a profile to edit.")
|
|
121
136
|
return
|
|
122
|
-
|
|
137
|
+
|
|
123
138
|
profile_id = current_item.data(Qt.UserRole)
|
|
124
139
|
profile = self.profile_service.get_profile(profile_id)
|
|
125
140
|
if not profile:
|
|
126
141
|
return
|
|
127
|
-
|
|
142
|
+
|
|
128
143
|
dialog = ProfileEditorDialog(self.profile_service, profile, parent=self)
|
|
129
144
|
dialog.exec()
|
|
130
|
-
|
|
145
|
+
|
|
131
146
|
def _delete_selected_profile(self):
|
|
132
147
|
"""Delete the selected profile."""
|
|
133
148
|
current_item = self.profile_list.currentItem()
|
|
134
149
|
if not current_item:
|
|
135
150
|
QMessageBox.warning(self, "No Selection", "Please select a profile to delete.")
|
|
136
151
|
return
|
|
137
|
-
|
|
152
|
+
|
|
138
153
|
profile_id = current_item.data(Qt.UserRole)
|
|
139
154
|
profile = self.profile_service.get_profile(profile_id)
|
|
140
155
|
if not profile:
|
|
141
156
|
return
|
|
142
|
-
|
|
157
|
+
|
|
143
158
|
reply = QMessageBox.question(
|
|
144
159
|
self,
|
|
145
160
|
"Delete Profile",
|
|
146
161
|
f"Delete profile '{profile.name}'?\n\nThis will also delete any saved credentials.",
|
|
147
|
-
QMessageBox.Yes | QMessageBox.No
|
|
162
|
+
QMessageBox.Yes | QMessageBox.No,
|
|
148
163
|
)
|
|
149
|
-
|
|
164
|
+
|
|
150
165
|
if reply == QMessageBox.Yes:
|
|
151
166
|
self.profile_service.delete_profile(profile_id)
|
|
152
|
-
|
|
167
|
+
|
|
153
168
|
def _create_profile(self):
|
|
154
169
|
"""Create a new profile."""
|
|
155
170
|
dialog = ProfileEditorDialog(self.profile_service, parent=self)
|
|
156
171
|
dialog.exec()
|
|
157
|
-
|
|
172
|
+
|
|
158
173
|
def _show_context_menu(self, pos):
|
|
159
174
|
"""Show context menu for profile."""
|
|
160
175
|
item = self.profile_list.itemAt(pos)
|
|
161
176
|
if not item:
|
|
162
177
|
return
|
|
163
|
-
|
|
178
|
+
|
|
164
179
|
profile_id = item.data(Qt.UserRole)
|
|
165
180
|
profile = self.profile_service.get_profile(profile_id)
|
|
166
181
|
if not profile:
|
|
167
182
|
return
|
|
168
|
-
|
|
183
|
+
|
|
169
184
|
menu = QMenu(self)
|
|
170
|
-
|
|
185
|
+
|
|
171
186
|
connect_action = menu.addAction("Connect")
|
|
172
187
|
connect_action.triggered.connect(lambda: self.connect_profile.emit(profile_id))
|
|
173
|
-
|
|
188
|
+
|
|
174
189
|
menu.addSeparator()
|
|
175
|
-
|
|
190
|
+
|
|
176
191
|
edit_action = menu.addAction("Edit")
|
|
177
192
|
edit_action.triggered.connect(lambda: self._edit_profile(profile_id))
|
|
178
|
-
|
|
193
|
+
|
|
179
194
|
duplicate_action = menu.addAction("Duplicate")
|
|
180
195
|
duplicate_action.triggered.connect(lambda: self._duplicate_profile(profile_id))
|
|
181
|
-
|
|
196
|
+
|
|
182
197
|
menu.addSeparator()
|
|
183
|
-
|
|
198
|
+
|
|
184
199
|
delete_action = menu.addAction("Delete")
|
|
185
200
|
delete_action.triggered.connect(lambda: self._delete_profile(profile_id))
|
|
186
|
-
|
|
201
|
+
|
|
187
202
|
menu.exec_(self.profile_list.mapToGlobal(pos))
|
|
188
|
-
|
|
203
|
+
|
|
189
204
|
def _edit_profile(self, profile_id: str):
|
|
190
205
|
"""Edit a profile."""
|
|
191
206
|
profile = self.profile_service.get_profile(profile_id)
|
|
192
207
|
if profile:
|
|
193
208
|
dialog = ProfileEditorDialog(self.profile_service, profile, parent=self)
|
|
194
209
|
dialog.exec()
|
|
195
|
-
|
|
210
|
+
|
|
196
211
|
def _duplicate_profile(self, profile_id: str):
|
|
197
212
|
"""Duplicate a profile."""
|
|
198
213
|
profile = self.profile_service.get_profile(profile_id)
|
|
199
214
|
if not profile:
|
|
200
215
|
return
|
|
201
|
-
|
|
216
|
+
|
|
202
217
|
from PySide6.QtWidgets import QInputDialog
|
|
218
|
+
|
|
203
219
|
new_name, ok = QInputDialog.getText(
|
|
204
220
|
self,
|
|
205
221
|
"Duplicate Profile",
|
|
206
222
|
"Enter name for duplicated profile:",
|
|
207
|
-
text=f"{profile.name} (Copy)"
|
|
223
|
+
text=f"{profile.name} (Copy)",
|
|
208
224
|
)
|
|
209
|
-
|
|
225
|
+
|
|
210
226
|
if ok and new_name:
|
|
211
227
|
self.profile_service.duplicate_profile(profile_id, new_name)
|
|
212
|
-
|
|
228
|
+
|
|
213
229
|
def _delete_profile(self, profile_id: str):
|
|
214
230
|
"""Delete a profile."""
|
|
215
231
|
profile = self.profile_service.get_profile(profile_id)
|
|
216
232
|
if not profile:
|
|
217
233
|
return
|
|
218
|
-
|
|
234
|
+
|
|
219
235
|
reply = QMessageBox.question(
|
|
220
236
|
self,
|
|
221
237
|
"Delete Profile",
|
|
222
238
|
f"Delete profile '{profile.name}'?",
|
|
223
|
-
QMessageBox.Yes | QMessageBox.No
|
|
239
|
+
QMessageBox.Yes | QMessageBox.No,
|
|
224
240
|
)
|
|
225
|
-
|
|
241
|
+
|
|
226
242
|
if reply == QMessageBox.Yes:
|
|
227
243
|
self.profile_service.delete_profile(profile_id)
|
|
228
244
|
|
|
229
245
|
|
|
246
|
+
class DatabaseFetchThread(QThread):
|
|
247
|
+
"""Background thread to fetch database names using PgVectorConnection."""
|
|
248
|
+
|
|
249
|
+
finished = Signal(list, str) # (databases, error)
|
|
250
|
+
|
|
251
|
+
def __init__(self, host: str, port: int, user: str, password: str, parent=None):
|
|
252
|
+
super().__init__(parent)
|
|
253
|
+
self.host = host
|
|
254
|
+
self.port = port
|
|
255
|
+
self.user = user
|
|
256
|
+
self.password = password
|
|
257
|
+
|
|
258
|
+
def run(self):
|
|
259
|
+
try:
|
|
260
|
+
from vector_inspector.core.connections.pgvector_connection import PgVectorConnection
|
|
261
|
+
|
|
262
|
+
conn = PgVectorConnection(
|
|
263
|
+
host=self.host,
|
|
264
|
+
port=int(self.port),
|
|
265
|
+
database="postgres",
|
|
266
|
+
user=self.user,
|
|
267
|
+
password=self.password,
|
|
268
|
+
)
|
|
269
|
+
if not conn.connect():
|
|
270
|
+
self.finished.emit([], "Failed to connect to server")
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
dbs = conn.list_databases()
|
|
274
|
+
conn.disconnect()
|
|
275
|
+
self.finished.emit(dbs or [], "")
|
|
276
|
+
except Exception as e:
|
|
277
|
+
self.finished.emit([], str(e))
|
|
278
|
+
|
|
279
|
+
|
|
230
280
|
class ProfileEditorDialog(QDialog):
|
|
231
281
|
"""Dialog for creating/editing connection profiles."""
|
|
232
|
-
|
|
233
|
-
def __init__(
|
|
282
|
+
|
|
283
|
+
def __init__(
|
|
284
|
+
self,
|
|
285
|
+
profile_service: ProfileService,
|
|
286
|
+
profile: Optional[ConnectionProfile] = None,
|
|
287
|
+
parent=None,
|
|
288
|
+
):
|
|
234
289
|
"""
|
|
235
290
|
Initialize profile editor dialog.
|
|
236
|
-
|
|
291
|
+
|
|
237
292
|
Args:
|
|
238
293
|
profile_service: The ProfileService instance
|
|
239
294
|
profile: Existing profile to edit (None for new profile)
|
|
@@ -243,64 +298,65 @@ class ProfileEditorDialog(QDialog):
|
|
|
243
298
|
self.profile_service = profile_service
|
|
244
299
|
self.profile = profile
|
|
245
300
|
self.is_edit_mode = profile is not None
|
|
246
|
-
|
|
301
|
+
|
|
247
302
|
self.setWindowTitle("Edit Profile" if self.is_edit_mode else "New Profile")
|
|
248
303
|
self.setMinimumWidth(500)
|
|
249
|
-
|
|
304
|
+
|
|
250
305
|
self._setup_ui()
|
|
251
|
-
|
|
306
|
+
|
|
252
307
|
if self.is_edit_mode:
|
|
253
308
|
self._load_profile_data()
|
|
254
|
-
|
|
309
|
+
|
|
255
310
|
def _setup_ui(self):
|
|
256
311
|
"""Setup the UI."""
|
|
257
312
|
layout = QVBoxLayout(self)
|
|
258
|
-
|
|
313
|
+
|
|
259
314
|
form_layout = QFormLayout()
|
|
260
|
-
|
|
315
|
+
|
|
261
316
|
# Profile name
|
|
262
317
|
self.name_input = QLineEdit()
|
|
263
318
|
form_layout.addRow("Profile Name:", self.name_input)
|
|
264
|
-
|
|
319
|
+
|
|
265
320
|
# Provider
|
|
266
321
|
self.provider_combo = QComboBox()
|
|
267
322
|
self.provider_combo.addItem("ChromaDB", "chromadb")
|
|
268
323
|
self.provider_combo.addItem("Qdrant", "qdrant")
|
|
324
|
+
self.provider_combo.addItem("PgVector/PostgreSQL", "pgvector")
|
|
269
325
|
self.provider_combo.addItem("Pinecone", "pinecone")
|
|
270
326
|
self.provider_combo.currentIndexChanged.connect(self._on_provider_changed)
|
|
271
327
|
form_layout.addRow("Provider:", self.provider_combo)
|
|
272
|
-
|
|
328
|
+
|
|
273
329
|
layout.addLayout(form_layout)
|
|
274
|
-
|
|
330
|
+
|
|
275
331
|
# Connection type group
|
|
276
332
|
type_group = QGroupBox("Connection Type")
|
|
277
333
|
type_layout = QVBoxLayout()
|
|
278
|
-
|
|
334
|
+
|
|
279
335
|
self.button_group = QButtonGroup()
|
|
280
|
-
|
|
336
|
+
|
|
281
337
|
self.persistent_radio = QRadioButton("Persistent (Local File)")
|
|
282
338
|
self.persistent_radio.setChecked(True)
|
|
283
339
|
self.persistent_radio.toggled.connect(self._on_type_changed)
|
|
284
|
-
|
|
340
|
+
|
|
285
341
|
self.http_radio = QRadioButton("HTTP (Remote Server)")
|
|
286
|
-
|
|
342
|
+
|
|
287
343
|
self.ephemeral_radio = QRadioButton("Ephemeral (In-Memory)")
|
|
288
|
-
|
|
344
|
+
|
|
289
345
|
self.button_group.addButton(self.persistent_radio)
|
|
290
346
|
self.button_group.addButton(self.http_radio)
|
|
291
347
|
self.button_group.addButton(self.ephemeral_radio)
|
|
292
|
-
|
|
348
|
+
|
|
293
349
|
type_layout.addWidget(self.persistent_radio)
|
|
294
350
|
type_layout.addWidget(self.http_radio)
|
|
295
351
|
type_layout.addWidget(self.ephemeral_radio)
|
|
296
352
|
type_group.setLayout(type_layout)
|
|
297
|
-
|
|
353
|
+
|
|
298
354
|
layout.addWidget(type_group)
|
|
299
|
-
|
|
355
|
+
|
|
300
356
|
# Connection details
|
|
301
357
|
details_group = QGroupBox("Connection Details")
|
|
302
358
|
details_layout = QFormLayout()
|
|
303
|
-
|
|
359
|
+
|
|
304
360
|
# Persistent path
|
|
305
361
|
self.path_layout = QHBoxLayout()
|
|
306
362
|
self.path_input = QLineEdit()
|
|
@@ -309,46 +365,78 @@ class ProfileEditorDialog(QDialog):
|
|
|
309
365
|
self.path_layout.addWidget(self.path_input)
|
|
310
366
|
self.path_layout.addWidget(self.path_browse_btn)
|
|
311
367
|
details_layout.addRow("Path:", self.path_layout)
|
|
312
|
-
|
|
368
|
+
|
|
313
369
|
# HTTP settings
|
|
314
370
|
self.host_input = QLineEdit("localhost")
|
|
315
371
|
details_layout.addRow("Host:", self.host_input)
|
|
316
|
-
|
|
372
|
+
|
|
317
373
|
self.port_input = QLineEdit("8000")
|
|
318
374
|
details_layout.addRow("Port:", self.port_input)
|
|
319
|
-
|
|
375
|
+
|
|
320
376
|
self.api_key_input = QLineEdit()
|
|
321
377
|
self.api_key_input.setEchoMode(QLineEdit.Password)
|
|
322
378
|
details_layout.addRow("API Key:", self.api_key_input)
|
|
323
|
-
|
|
379
|
+
|
|
380
|
+
# (Database field moved to end of form)
|
|
381
|
+
|
|
382
|
+
self.user_input = QLineEdit()
|
|
383
|
+
details_layout.addRow("User:", self.user_input)
|
|
384
|
+
|
|
385
|
+
self.password_input = QLineEdit()
|
|
386
|
+
self.password_input.setEchoMode(QLineEdit.Password)
|
|
387
|
+
details_layout.addRow("Password:", self.password_input)
|
|
388
|
+
|
|
389
|
+
# PgVector/Postgres specific: editable combo + refresh (placed last)
|
|
390
|
+
self.database_input = QComboBox()
|
|
391
|
+
self.database_input.setEditable(True)
|
|
392
|
+
db_layout = QHBoxLayout()
|
|
393
|
+
db_layout.addWidget(self.database_input)
|
|
394
|
+
self.db_refresh_btn = QPushButton("⟳")
|
|
395
|
+
self.db_refresh_btn.setToolTip("Refresh database list")
|
|
396
|
+
self.db_refresh_btn.setMaximumWidth(30)
|
|
397
|
+
self.db_refresh_btn.clicked.connect(lambda: self._fetch_databases())
|
|
398
|
+
db_layout.addWidget(self.db_refresh_btn)
|
|
399
|
+
self.db_status_label = QLabel("")
|
|
400
|
+
self.db_status_label.setStyleSheet("color: gray; padding-left: 6px;")
|
|
401
|
+
db_layout.addWidget(self.db_status_label)
|
|
402
|
+
# Provide a clear hint: user should click "Test Connection" to fetch DB list
|
|
403
|
+
line = self.database_input.lineEdit()
|
|
404
|
+
if line is not None:
|
|
405
|
+
line.setPlaceholderText("Click 'Test Connection' to fetch databases")
|
|
406
|
+
# Disable the refresh button until a fetch has occurred
|
|
407
|
+
self.db_refresh_btn.setEnabled(False)
|
|
408
|
+
# Small hint in the status label
|
|
409
|
+
self.db_status_label.setText("Click 'Test Connection' to load databases")
|
|
410
|
+
details_layout.addRow("Database:", db_layout)
|
|
411
|
+
|
|
324
412
|
details_group.setLayout(details_layout)
|
|
325
413
|
layout.addWidget(details_group)
|
|
326
|
-
|
|
414
|
+
|
|
327
415
|
# Test connection button
|
|
328
416
|
self.test_btn = QPushButton("Test Connection")
|
|
329
417
|
self.test_btn.clicked.connect(self._test_connection)
|
|
330
418
|
layout.addWidget(self.test_btn)
|
|
331
|
-
|
|
419
|
+
|
|
332
420
|
# Buttons
|
|
333
421
|
button_layout = QHBoxLayout()
|
|
334
|
-
|
|
422
|
+
|
|
335
423
|
self.save_btn = QPushButton("Save")
|
|
336
424
|
self.save_btn.clicked.connect(self._save_profile)
|
|
337
425
|
button_layout.addWidget(self.save_btn)
|
|
338
|
-
|
|
426
|
+
|
|
339
427
|
self.cancel_btn = QPushButton("Cancel")
|
|
340
428
|
self.cancel_btn.clicked.connect(self.reject)
|
|
341
429
|
button_layout.addWidget(self.cancel_btn)
|
|
342
|
-
|
|
430
|
+
|
|
343
431
|
layout.addLayout(button_layout)
|
|
344
|
-
|
|
432
|
+
|
|
345
433
|
# Initial state
|
|
346
434
|
self._on_type_changed()
|
|
347
|
-
|
|
435
|
+
|
|
348
436
|
def _on_provider_changed(self):
|
|
349
437
|
"""Handle provider change."""
|
|
350
438
|
provider = self.provider_combo.currentData()
|
|
351
|
-
|
|
439
|
+
|
|
352
440
|
# Update default port
|
|
353
441
|
if provider == "qdrant":
|
|
354
442
|
if self.port_input.text() == "8000":
|
|
@@ -356,7 +444,10 @@ class ProfileEditorDialog(QDialog):
|
|
|
356
444
|
elif provider == "chromadb":
|
|
357
445
|
if self.port_input.text() == "6333":
|
|
358
446
|
self.port_input.setText("8000")
|
|
359
|
-
|
|
447
|
+
elif provider == "pgvector":
|
|
448
|
+
if self.port_input.text() in ("8000", "6333"):
|
|
449
|
+
self.port_input.setText("5432")
|
|
450
|
+
|
|
360
451
|
# For Pinecone, disable persistent/ephemeral modes and only show API key
|
|
361
452
|
if provider == "pinecone":
|
|
362
453
|
self.persistent_radio.setEnabled(False)
|
|
@@ -368,6 +459,20 @@ class ProfileEditorDialog(QDialog):
|
|
|
368
459
|
self.host_input.setEnabled(False)
|
|
369
460
|
self.port_input.setEnabled(False)
|
|
370
461
|
self.api_key_input.setEnabled(True)
|
|
462
|
+
elif provider == "pgvector":
|
|
463
|
+
# PgVector uses host/port/database/user/password; prefer HTTP type
|
|
464
|
+
self.persistent_radio.setEnabled(False)
|
|
465
|
+
self.http_radio.setEnabled(True)
|
|
466
|
+
self.http_radio.setChecked(True)
|
|
467
|
+
self.ephemeral_radio.setEnabled(False)
|
|
468
|
+
self.path_input.setEnabled(False)
|
|
469
|
+
self.path_browse_btn.setEnabled(False)
|
|
470
|
+
self.host_input.setEnabled(True)
|
|
471
|
+
self.port_input.setEnabled(True)
|
|
472
|
+
self.api_key_input.setEnabled(False)
|
|
473
|
+
self.database_input.setEnabled(True)
|
|
474
|
+
self.user_input.setEnabled(True)
|
|
475
|
+
self.password_input.setEnabled(True)
|
|
371
476
|
else:
|
|
372
477
|
self.persistent_radio.setEnabled(True)
|
|
373
478
|
self.http_radio.setEnabled(True)
|
|
@@ -377,14 +482,14 @@ class ProfileEditorDialog(QDialog):
|
|
|
377
482
|
self.api_key_input.setEnabled(is_http and provider == "qdrant")
|
|
378
483
|
# Update other fields based on connection type
|
|
379
484
|
self._on_type_changed()
|
|
380
|
-
|
|
485
|
+
|
|
381
486
|
def _on_type_changed(self):
|
|
382
487
|
"""Handle connection type change."""
|
|
383
488
|
is_persistent = self.persistent_radio.isChecked()
|
|
384
489
|
is_http = self.http_radio.isChecked()
|
|
385
|
-
|
|
490
|
+
|
|
386
491
|
provider = self.provider_combo.currentData()
|
|
387
|
-
|
|
492
|
+
|
|
388
493
|
# Pinecone always uses API key, no path/host/port
|
|
389
494
|
if provider == "pinecone":
|
|
390
495
|
self.path_input.setEnabled(False)
|
|
@@ -392,6 +497,16 @@ class ProfileEditorDialog(QDialog):
|
|
|
392
497
|
self.host_input.setEnabled(False)
|
|
393
498
|
self.port_input.setEnabled(False)
|
|
394
499
|
self.api_key_input.setEnabled(True)
|
|
500
|
+
elif provider == "pgvector":
|
|
501
|
+
# PgVector uses HTTP-like host/port + DB credentials
|
|
502
|
+
self.path_input.setEnabled(False)
|
|
503
|
+
self.path_browse_btn.setEnabled(False)
|
|
504
|
+
self.host_input.setEnabled(is_http)
|
|
505
|
+
self.port_input.setEnabled(is_http)
|
|
506
|
+
self.api_key_input.setEnabled(False)
|
|
507
|
+
self.database_input.setEnabled(is_http)
|
|
508
|
+
self.user_input.setEnabled(is_http)
|
|
509
|
+
self.password_input.setEnabled(is_http)
|
|
395
510
|
else:
|
|
396
511
|
# Show/hide relevant fields
|
|
397
512
|
self.path_input.setEnabled(is_persistent)
|
|
@@ -399,37 +514,35 @@ class ProfileEditorDialog(QDialog):
|
|
|
399
514
|
self.host_input.setEnabled(is_http)
|
|
400
515
|
self.port_input.setEnabled(is_http)
|
|
401
516
|
self.api_key_input.setEnabled(is_http and provider == "qdrant")
|
|
402
|
-
|
|
517
|
+
|
|
403
518
|
def _browse_for_path(self):
|
|
404
519
|
"""Browse for persistent storage path."""
|
|
405
520
|
path = QFileDialog.getExistingDirectory(
|
|
406
|
-
self,
|
|
407
|
-
"Select Database Directory",
|
|
408
|
-
self.path_input.text()
|
|
521
|
+
self, "Select Database Directory", self.path_input.text()
|
|
409
522
|
)
|
|
410
523
|
if path:
|
|
411
524
|
self.path_input.setText(path)
|
|
412
|
-
|
|
525
|
+
|
|
413
526
|
def _load_profile_data(self):
|
|
414
527
|
"""Load existing profile data into form."""
|
|
415
528
|
if not self.profile:
|
|
416
529
|
return
|
|
417
|
-
|
|
530
|
+
|
|
418
531
|
# Get profile with credentials
|
|
419
532
|
profile_data = self.profile_service.get_profile_with_credentials(self.profile.id)
|
|
420
533
|
if not profile_data:
|
|
421
534
|
return
|
|
422
|
-
|
|
535
|
+
|
|
423
536
|
self.name_input.setText(profile_data["name"])
|
|
424
|
-
|
|
537
|
+
|
|
425
538
|
# Set provider
|
|
426
539
|
index = self.provider_combo.findData(profile_data["provider"])
|
|
427
540
|
if index >= 0:
|
|
428
541
|
self.provider_combo.setCurrentIndex(index)
|
|
429
|
-
|
|
542
|
+
|
|
430
543
|
config = profile_data.get("config", {})
|
|
431
544
|
conn_type = config.get("type", "persistent")
|
|
432
|
-
|
|
545
|
+
|
|
433
546
|
# Set connection type
|
|
434
547
|
if conn_type == "cloud":
|
|
435
548
|
# Pinecone cloud connection
|
|
@@ -441,25 +554,32 @@ class ProfileEditorDialog(QDialog):
|
|
|
441
554
|
self.http_radio.setChecked(True)
|
|
442
555
|
self.host_input.setText(config.get("host", "localhost"))
|
|
443
556
|
self.port_input.setText(str(config.get("port", "8000")))
|
|
557
|
+
# PgVector HTTP-style config may include DB credentials
|
|
558
|
+
self.database_input.setCurrentText(config.get("database", ""))
|
|
559
|
+
self.user_input.setText(config.get("user", ""))
|
|
444
560
|
else:
|
|
445
561
|
self.ephemeral_radio.setChecked(True)
|
|
446
|
-
|
|
562
|
+
|
|
447
563
|
# Load credentials
|
|
448
564
|
credentials = profile_data.get("credentials", {})
|
|
449
565
|
if "api_key" in credentials:
|
|
450
566
|
self.api_key_input.setText(credentials["api_key"])
|
|
451
|
-
|
|
567
|
+
# pgvector may store password in credentials
|
|
568
|
+
if "password" in credentials:
|
|
569
|
+
self.password_input.setText(credentials["password"])
|
|
570
|
+
|
|
452
571
|
def _test_connection(self):
|
|
453
572
|
"""Test the connection with current settings."""
|
|
454
573
|
# Get config
|
|
455
574
|
config = self._get_config()
|
|
456
575
|
provider = self.provider_combo.currentData()
|
|
457
|
-
|
|
576
|
+
|
|
458
577
|
# Create connection
|
|
459
578
|
from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
|
|
460
579
|
from vector_inspector.core.connections.qdrant_connection import QdrantConnection
|
|
461
580
|
from vector_inspector.core.connections.pinecone_connection import PineconeConnection
|
|
462
|
-
|
|
581
|
+
from vector_inspector.core.connections.pgvector_connection import PgVectorConnection
|
|
582
|
+
|
|
463
583
|
try:
|
|
464
584
|
if provider == "pinecone":
|
|
465
585
|
api_key = self.api_key_input.text()
|
|
@@ -469,30 +589,44 @@ class ProfileEditorDialog(QDialog):
|
|
|
469
589
|
conn = PineconeConnection(api_key=api_key)
|
|
470
590
|
elif provider == "chromadb":
|
|
471
591
|
conn = ChromaDBConnection(**self._get_connection_kwargs(config))
|
|
592
|
+
elif provider == "pgvector":
|
|
593
|
+
conn = PgVectorConnection(
|
|
594
|
+
host=self.host_input.text(),
|
|
595
|
+
port=int(self.port_input.text()),
|
|
596
|
+
database=self.database_input.currentText(),
|
|
597
|
+
user=self.user_input.text(),
|
|
598
|
+
password=self.password_input.text(),
|
|
599
|
+
)
|
|
472
600
|
else:
|
|
473
601
|
conn = QdrantConnection(**self._get_connection_kwargs(config))
|
|
474
|
-
|
|
602
|
+
|
|
475
603
|
# Test connection
|
|
476
604
|
progress = QProgressDialog("Testing connection...", None, 0, 0, self)
|
|
477
605
|
progress.setWindowModality(Qt.WindowModal)
|
|
478
606
|
progress.show()
|
|
479
|
-
|
|
607
|
+
|
|
480
608
|
success = conn.connect()
|
|
481
609
|
progress.close()
|
|
482
|
-
|
|
610
|
+
|
|
483
611
|
if success:
|
|
484
612
|
QMessageBox.information(self, "Success", "Connection test successful!")
|
|
485
613
|
conn.disconnect()
|
|
614
|
+
# For pgvector, populate database suggestions after a successful connect
|
|
615
|
+
if provider == "pgvector":
|
|
616
|
+
try:
|
|
617
|
+
self._fetch_databases()
|
|
618
|
+
except Exception:
|
|
619
|
+
pass
|
|
486
620
|
else:
|
|
487
621
|
QMessageBox.warning(self, "Failed", "Connection test failed.")
|
|
488
622
|
except Exception as e:
|
|
489
623
|
QMessageBox.critical(self, "Error", f"Connection test error: {e}")
|
|
490
|
-
|
|
624
|
+
|
|
491
625
|
def _get_config(self) -> dict:
|
|
492
626
|
"""Get configuration from form."""
|
|
493
627
|
config = {}
|
|
494
628
|
provider = self.provider_combo.currentData()
|
|
495
|
-
|
|
629
|
+
|
|
496
630
|
# Pinecone uses cloud connection type
|
|
497
631
|
if provider == "pinecone":
|
|
498
632
|
config["type"] = "cloud"
|
|
@@ -503,15 +637,20 @@ class ProfileEditorDialog(QDialog):
|
|
|
503
637
|
config["type"] = "http"
|
|
504
638
|
config["host"] = self.host_input.text()
|
|
505
639
|
config["port"] = int(self.port_input.text())
|
|
640
|
+
# Include DB credentials for providers like pgvector
|
|
641
|
+
if self.database_input.currentText():
|
|
642
|
+
config["database"] = self.database_input.currentText()
|
|
643
|
+
if self.user_input.text():
|
|
644
|
+
config["user"] = self.user_input.text()
|
|
506
645
|
else:
|
|
507
646
|
config["type"] = "ephemeral"
|
|
508
|
-
|
|
647
|
+
|
|
509
648
|
return config
|
|
510
|
-
|
|
649
|
+
|
|
511
650
|
def _get_connection_kwargs(self, config: dict) -> dict:
|
|
512
651
|
"""Get kwargs for creating connection."""
|
|
513
652
|
kwargs = {}
|
|
514
|
-
|
|
653
|
+
|
|
515
654
|
if config["type"] == "persistent":
|
|
516
655
|
kwargs["path"] = config["path"]
|
|
517
656
|
elif config["type"] == "http":
|
|
@@ -519,19 +658,26 @@ class ProfileEditorDialog(QDialog):
|
|
|
519
658
|
kwargs["port"] = config["port"]
|
|
520
659
|
if self.api_key_input.text():
|
|
521
660
|
kwargs["api_key"] = self.api_key_input.text()
|
|
522
|
-
|
|
661
|
+
# Include DB credentials if present (pgvector)
|
|
662
|
+
if config.get("database"):
|
|
663
|
+
kwargs["database"] = config.get("database")
|
|
664
|
+
if config.get("user"):
|
|
665
|
+
kwargs["user"] = config.get("user")
|
|
666
|
+
if self.password_input.text():
|
|
667
|
+
kwargs["password"] = self.password_input.text()
|
|
668
|
+
|
|
523
669
|
return kwargs
|
|
524
|
-
|
|
670
|
+
|
|
525
671
|
def _save_profile(self):
|
|
526
672
|
"""Save the profile."""
|
|
527
673
|
name = self.name_input.text().strip()
|
|
528
674
|
if not name:
|
|
529
675
|
QMessageBox.warning(self, "Invalid Input", "Please enter a profile name.")
|
|
530
676
|
return
|
|
531
|
-
|
|
677
|
+
|
|
532
678
|
provider = self.provider_combo.currentData()
|
|
533
679
|
config = self._get_config()
|
|
534
|
-
|
|
680
|
+
|
|
535
681
|
# Get credentials
|
|
536
682
|
credentials = {}
|
|
537
683
|
if provider == "pinecone":
|
|
@@ -543,14 +689,16 @@ class ProfileEditorDialog(QDialog):
|
|
|
543
689
|
return
|
|
544
690
|
elif self.api_key_input.text() and self.http_radio.isChecked():
|
|
545
691
|
credentials["api_key"] = self.api_key_input.text()
|
|
546
|
-
|
|
692
|
+
elif provider == "pgvector" and self.password_input.text() and self.http_radio.isChecked():
|
|
693
|
+
credentials["password"] = self.password_input.text()
|
|
694
|
+
|
|
547
695
|
if self.is_edit_mode:
|
|
548
696
|
# Update existing profile
|
|
549
697
|
self.profile_service.update_profile(
|
|
550
698
|
self.profile.id,
|
|
551
699
|
name=name,
|
|
552
700
|
config=config,
|
|
553
|
-
credentials=credentials if credentials else None
|
|
701
|
+
credentials=credentials if credentials else None,
|
|
554
702
|
)
|
|
555
703
|
else:
|
|
556
704
|
# Create new profile
|
|
@@ -558,8 +706,54 @@ class ProfileEditorDialog(QDialog):
|
|
|
558
706
|
name=name,
|
|
559
707
|
provider=provider,
|
|
560
708
|
config=config,
|
|
561
|
-
credentials=credentials if credentials else None
|
|
709
|
+
credentials=credentials if credentials else None,
|
|
562
710
|
)
|
|
563
|
-
|
|
711
|
+
|
|
564
712
|
self.accept()
|
|
565
713
|
|
|
714
|
+
def _fetch_databases(self):
|
|
715
|
+
"""Start background fetch of database names."""
|
|
716
|
+
host = self.host_input.text()
|
|
717
|
+
try:
|
|
718
|
+
port = int(self.port_input.text())
|
|
719
|
+
except Exception:
|
|
720
|
+
port = 5432
|
|
721
|
+
user = self.user_input.text()
|
|
722
|
+
password = self.password_input.text()
|
|
723
|
+
|
|
724
|
+
# Disable refresh while fetching
|
|
725
|
+
try:
|
|
726
|
+
self.db_refresh_btn.setEnabled(False)
|
|
727
|
+
self.db_status_label.setText("Fetching…")
|
|
728
|
+
except Exception:
|
|
729
|
+
pass
|
|
730
|
+
|
|
731
|
+
self._db_thread = DatabaseFetchThread(host=host, port=port, user=user, password=password)
|
|
732
|
+
self._db_thread.finished.connect(self._on_databases_fetched)
|
|
733
|
+
self._db_thread.start()
|
|
734
|
+
|
|
735
|
+
def _on_databases_fetched(self, dbs: list, error: str):
|
|
736
|
+
try:
|
|
737
|
+
self.db_refresh_btn.setEnabled(True)
|
|
738
|
+
except Exception:
|
|
739
|
+
pass
|
|
740
|
+
|
|
741
|
+
if dbs:
|
|
742
|
+
# Preserve current text if set
|
|
743
|
+
current = (
|
|
744
|
+
self.database_input.currentText()
|
|
745
|
+
if hasattr(self.database_input, "currentText")
|
|
746
|
+
else ""
|
|
747
|
+
)
|
|
748
|
+
self.database_input.clear()
|
|
749
|
+
self.database_input.addItems(dbs)
|
|
750
|
+
if current:
|
|
751
|
+
self.database_input.setCurrentText(current)
|
|
752
|
+
self.db_status_label.setText(f"Loaded {len(dbs)} databases")
|
|
753
|
+
else:
|
|
754
|
+
# If fetch failed and we have an error, show a non-blocking warning
|
|
755
|
+
if error:
|
|
756
|
+
self.db_status_label.setText("Failed to fetch databases")
|
|
757
|
+
QMessageBox.warning(self, "Database List", f"Could not fetch databases: {error}")
|
|
758
|
+
else:
|
|
759
|
+
self.db_status_label.setText("")
|