vector-inspector 0.3.4__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.
@@ -2,238 +2,293 @@
2
2
 
3
3
  from typing import Optional
4
4
  from PySide6.QtWidgets import (
5
- QWidget, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem,
6
- QPushButton, QMenu, QMessageBox, QLabel, QDialog, QFormLayout,
7
- QLineEdit, QComboBox, QRadioButton, QButtonGroup, QGroupBox,
8
- QFileDialog, QCheckBox, QProgressDialog
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__(self, profile_service: ProfileService, profile: Optional[ConnectionProfile] = None, parent=None):
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("")