vector-inspector 0.3.6__py3-none-any.whl → 0.3.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -489,7 +489,7 @@ class ChromaDBConnection(VectorDBConnection):
489
489
  name: Collection name
490
490
 
491
491
  Returns:
492
- True if successful, False otherwise
492
+ True if successful or collection does not exist, False otherwise
493
493
  """
494
494
  if not self._client:
495
495
  return False
@@ -500,6 +500,9 @@ class ChromaDBConnection(VectorDBConnection):
500
500
  self._current_collection = None
501
501
  return True
502
502
  except Exception as e:
503
+ # If the exception is about the collection not existing, treat as success (idempotent)
504
+ if "does not exist" in str(e).lower():
505
+ return True
503
506
  log_error("Failed to delete collection: %s", e)
504
507
  return False
505
508
 
@@ -1,6 +1,6 @@
1
1
  """PgVector/PostgreSQL connection manager."""
2
2
 
3
- from typing import Optional, List, Dict, Any
3
+ from typing import Any
4
4
  import json
5
5
  import psycopg2
6
6
  from psycopg2 import sql
@@ -36,7 +36,7 @@ class PgVectorConnection(VectorDBConnection):
36
36
  self.database = database
37
37
  self.user = user
38
38
  self.password = password
39
- self._client: Optional[psycopg2.extensions.connection] = None
39
+ self._client = None
40
40
  # Track how many embeddings were regenerated by the last update operation
41
41
  self._last_regenerated_count: int = 0
42
42
 
@@ -94,7 +94,7 @@ class PgVectorConnection(VectorDBConnection):
94
94
  """Check if connected to PostgreSQL."""
95
95
  return self._client is not None
96
96
 
97
- def list_collections(self) -> List[str]:
97
+ def list_collections(self) -> list[str]:
98
98
  """
99
99
  Get list of all vector tables (collections).
100
100
 
@@ -117,7 +117,7 @@ class PgVectorConnection(VectorDBConnection):
117
117
  log_error("Failed to list collections: %s", e)
118
118
  return []
119
119
 
120
- def list_databases(self) -> List[str]:
120
+ def list_databases(self) -> list[str]:
121
121
  """
122
122
  List available databases on the server (non-template databases).
123
123
 
@@ -155,7 +155,7 @@ class PgVectorConnection(VectorDBConnection):
155
155
  except Exception:
156
156
  pass
157
157
 
158
- def get_collection_info(self, name: str) -> Optional[Dict[str, Any]]:
158
+ def get_collection_info(self, name: str) -> dict[str, Any] | None:
159
159
  """
160
160
  Get collection metadata and statistics.
161
161
 
@@ -291,10 +291,10 @@ class PgVectorConnection(VectorDBConnection):
291
291
  def add_items(
292
292
  self,
293
293
  collection_name: str,
294
- documents: List[str],
295
- metadatas: Optional[List[Dict[str, Any]]] = None,
296
- ids: Optional[List[str]] = None,
297
- embeddings: Optional[List[List[float]]] = None,
294
+ documents: list[str],
295
+ metadatas: list[dict[str, Any]] | None = None,
296
+ ids: list[str] | None = None,
297
+ embeddings: list[list[float]] | None = None,
298
298
  ) -> bool:
299
299
  """
300
300
  Add items to a collection.
@@ -312,66 +312,16 @@ class PgVectorConnection(VectorDBConnection):
312
312
  if not self._client:
313
313
  return False
314
314
 
315
- # If embeddings weren't provided, try to compute them using configured/default model
315
+ # If embeddings weren't provided, try to compute them using the helper method
316
316
  if not embeddings:
317
317
  try:
318
- from vector_inspector.services.settings_service import SettingsService
319
- from vector_inspector.core.embedding_utils import (
320
- load_embedding_model,
321
- get_embedding_model_for_dimension,
322
- DEFAULT_MODEL,
323
- encode_text,
324
- )
325
-
326
- model_name = None
327
- model_type = None
328
-
329
- # 1) settings
330
- settings = SettingsService()
331
- model_info = settings.get_embedding_model(self.database, collection_name)
332
- if model_info:
333
- model_name = model_info.get("model")
334
- model_type = model_info.get("type", "sentence-transformer")
335
-
336
- # 2) collection metadata
337
- coll_info = None
338
- if not model_name:
339
- coll_info = self.get_collection_info(collection_name)
340
- if coll_info and coll_info.get("embedding_model"):
341
- model_name = coll_info.get("embedding_model")
342
- model_type = coll_info.get("embedding_model_type", "stored")
343
-
344
- # 3) dimension-based fallback
345
- loaded_model = None
346
- if not model_name:
347
- # Try to get vector dimension
348
- dim = None
349
- if not coll_info:
350
- coll_info = self.get_collection_info(collection_name)
351
- if coll_info and coll_info.get("vector_dimension"):
352
- try:
353
- dim = int(coll_info.get("vector_dimension"))
354
- except Exception:
355
- dim = None
356
- if dim:
357
- loaded_model, model_name, model_type = get_embedding_model_for_dimension(
358
- dim
359
- )
360
- else:
361
- model_name, model_type = DEFAULT_MODEL
362
-
363
- # Load model
364
- if not loaded_model:
365
- loaded_model = load_embedding_model(model_name, model_type)
366
-
367
- # Compute embeddings for all documents
368
- if model_type != "clip":
369
- embeddings = loaded_model.encode(documents, show_progress_bar=False).tolist()
370
- else:
371
- embeddings = [encode_text(d, loaded_model, model_type) for d in documents]
318
+ embeddings = self.compute_embeddings_for_documents(collection_name, documents)
372
319
  except Exception as e:
373
320
  log_error("Failed to compute embeddings on add: %s", e)
374
321
  return False
322
+ if embeddings is None:
323
+ return False
324
+
375
325
  try:
376
326
  import uuid
377
327
 
@@ -428,7 +378,7 @@ class PgVectorConnection(VectorDBConnection):
428
378
  self._client.rollback()
429
379
  return False
430
380
 
431
- def get_items(self, name: str, ids: List[str]) -> Dict[str, Any]:
381
+ def get_items(self, name: str, ids: list[str]) -> dict[str, Any]:
432
382
  """
433
383
  Retrieve items by IDs.
434
384
 
@@ -548,12 +498,12 @@ class PgVectorConnection(VectorDBConnection):
548
498
  def query_collection(
549
499
  self,
550
500
  collection_name: str,
551
- query_texts: Optional[List[str]] = None,
552
- query_embeddings: Optional[List[List[float]]] = None,
501
+ query_texts: list[str] | None = None,
502
+ query_embeddings: list[list[float]] | None = None,
553
503
  n_results: int = 10,
554
- where: Optional[Dict[str, Any]] = None,
555
- where_document: Optional[Dict[str, Any]] = None,
556
- ) -> Optional[Dict[str, Any]]:
504
+ where: dict[str, Any] | None = None,
505
+ where_document: dict[str, Any] | None = None,
506
+ ) -> dict[str, Any] | None:
557
507
  """
558
508
  Query a collection for similar vectors.
559
509
 
@@ -601,6 +551,7 @@ class PgVectorConnection(VectorDBConnection):
601
551
  # 3) dimension-based fallback
602
552
  loaded_model = None
603
553
  if not model_name:
554
+ # Try to get vector dimension
604
555
  dim = None
605
556
  coll_info = self.get_collection_info(collection_name)
606
557
  if coll_info and coll_info.get("vector_dimension"):
@@ -613,6 +564,7 @@ class PgVectorConnection(VectorDBConnection):
613
564
  dim
614
565
  )
615
566
  else:
567
+ # Use default model
616
568
  model_name, model_type = DEFAULT_MODEL
617
569
 
618
570
  if not loaded_model:
@@ -636,11 +588,11 @@ class PgVectorConnection(VectorDBConnection):
636
588
  # so callers receive the top-N results per query (matching SearchView expectations).
637
589
  with self._client.cursor() as cur:
638
590
  # Prepare containers for per-query results
639
- per_ids: List[List[str]] = []
640
- per_docs: List[List[str]] = []
641
- per_metas: List[List[Dict[str, Any]]] = []
642
- per_embeds: List[List[List[float]]] = []
643
- per_dists: List[List[float]] = []
591
+ per_ids = []
592
+ per_docs = []
593
+ per_metas = []
594
+ per_embeds = []
595
+ per_dists = []
644
596
 
645
597
  for emb in query_embeddings:
646
598
  # Build base query for this single embedding
@@ -675,11 +627,11 @@ class PgVectorConnection(VectorDBConnection):
675
627
  colnames = [desc[0] for desc in cur.description]
676
628
 
677
629
  # Build per-query result lists
678
- ids_q: List[str] = []
679
- docs_q: List[str] = []
680
- metas_q: List[Dict[str, Any]] = []
681
- embeds_q: List[List[float]] = []
682
- dists_q: List[float] = []
630
+ ids_q = []
631
+ docs_q = []
632
+ metas_q = []
633
+ embeds_q = []
634
+ dists_q = []
683
635
 
684
636
  for row in rows:
685
637
  row_dict = dict(zip(colnames, row))
@@ -731,10 +683,10 @@ class PgVectorConnection(VectorDBConnection):
731
683
  def get_all_items(
732
684
  self,
733
685
  collection_name: str,
734
- limit: Optional[int] = None,
735
- offset: Optional[int] = None,
736
- where: Optional[Dict[str, Any]] = None,
737
- ) -> Optional[Dict[str, Any]]:
686
+ limit: int | None = None,
687
+ offset: int | None = None,
688
+ where: dict[str, Any] | None = None,
689
+ ) -> dict[str, Any] | None:
738
690
  """
739
691
  Get all items from a collection.
740
692
 
@@ -835,10 +787,10 @@ class PgVectorConnection(VectorDBConnection):
835
787
  def update_items(
836
788
  self,
837
789
  collection_name: str,
838
- ids: List[str],
839
- documents: Optional[List[str]] = None,
840
- metadatas: Optional[List[Dict[str, Any]]] = None,
841
- embeddings: Optional[List[List[float]]] = None,
790
+ ids: list[str],
791
+ documents: list[str] | None = None,
792
+ metadatas: list[dict[str, Any]] | None = None,
793
+ embeddings: list[list[float]] | None = None,
842
794
  ) -> bool:
843
795
  """
844
796
  Update items in a collection.
@@ -1002,8 +954,8 @@ class PgVectorConnection(VectorDBConnection):
1002
954
  def delete_items(
1003
955
  self,
1004
956
  collection_name: str,
1005
- ids: Optional[List[str]] = None,
1006
- where: Optional[Dict[str, Any]] = None,
957
+ ids: list[str] | None = None,
958
+ where: dict[str, Any] | None = None,
1007
959
  ) -> bool:
1008
960
  """
1009
961
  Delete items from a collection.
@@ -1034,7 +986,7 @@ class PgVectorConnection(VectorDBConnection):
1034
986
  self._client.rollback()
1035
987
  return False
1036
988
 
1037
- def get_connection_info(self) -> Dict[str, Any]:
989
+ def get_connection_info(self) -> dict[str, Any]:
1038
990
  """
1039
991
  Get information about the current connection.
1040
992
 
@@ -1050,7 +1002,7 @@ class PgVectorConnection(VectorDBConnection):
1050
1002
  "connected": self.is_connected,
1051
1003
  }
1052
1004
 
1053
- def _get_table_schema(self, table_name: str) -> Dict[str, str]:
1005
+ def _get_table_schema(self, table_name: str) -> dict[str, str]:
1054
1006
  """
1055
1007
  Get the schema (column names and types) for a table.
1056
1008
 
@@ -1081,7 +1033,7 @@ class PgVectorConnection(VectorDBConnection):
1081
1033
  log_error("Failed to get table schema: %s", e)
1082
1034
  return {}
1083
1035
 
1084
- def _parse_vector(self, vector_str: Any) -> List[float]:
1036
+ def _parse_vector(self, vector_str: Any) -> list[float]:
1085
1037
  """
1086
1038
  Parse pgvector string format to Python list.
1087
1039
 
@@ -1098,3 +1050,66 @@ class PgVectorConnection(VectorDBConnection):
1098
1050
  vector_str = vector_str.strip("[]")
1099
1051
  return [float(x) for x in vector_str.split(",")]
1100
1052
  return []
1053
+
1054
+ def compute_embeddings_for_documents(
1055
+ self, collection_name: str, documents: list[str]
1056
+ ) -> list[list[float]] | None:
1057
+ """
1058
+ Compute embeddings for a list of documents using the configured/default model for the collection.
1059
+ Returns a list of embeddings, or None on failure.
1060
+ """
1061
+ try:
1062
+ from vector_inspector.services.settings_service import SettingsService
1063
+ from vector_inspector.core.embedding_utils import (
1064
+ load_embedding_model,
1065
+ get_embedding_model_for_dimension,
1066
+ DEFAULT_MODEL,
1067
+ encode_text,
1068
+ )
1069
+
1070
+ model_name = None
1071
+ model_type = None
1072
+
1073
+ # 1) settings
1074
+ settings = SettingsService()
1075
+ model_info = settings.get_embedding_model(self.database, collection_name)
1076
+ if model_info:
1077
+ model_name = model_info.get("model")
1078
+ model_type = model_info.get("type", "sentence-transformer")
1079
+
1080
+ # 2) collection metadata
1081
+ if not model_name:
1082
+ coll_info = self.get_collection_info(collection_name)
1083
+ if coll_info and coll_info.get("embedding_model"):
1084
+ model_name = coll_info.get("embedding_model")
1085
+ model_type = coll_info.get("embedding_model_type", "stored")
1086
+
1087
+ # 3) dimension-based fallback
1088
+ loaded_model = None
1089
+ if not model_name:
1090
+ # Try to get vector dimension
1091
+ dim = None
1092
+ coll_info = self.get_collection_info(collection_name)
1093
+ if coll_info and coll_info.get("vector_dimension"):
1094
+ try:
1095
+ dim = int(coll_info.get("vector_dimension"))
1096
+ except Exception:
1097
+ dim = None
1098
+ if dim:
1099
+ loaded_model, model_name, model_type = get_embedding_model_for_dimension(dim)
1100
+ else:
1101
+ model_name, model_type = DEFAULT_MODEL
1102
+
1103
+ # Load model
1104
+ if not loaded_model:
1105
+ loaded_model = load_embedding_model(model_name, model_type)
1106
+
1107
+ # Compute embeddings for all documents
1108
+ if model_type != "clip":
1109
+ embeddings = loaded_model.encode(documents, show_progress_bar=False).tolist()
1110
+ else:
1111
+ embeddings = [encode_text(d, loaded_model, model_type) for d in documents]
1112
+ return embeddings
1113
+ except Exception as e:
1114
+ log_error("Failed to compute embeddings: %s", e)
1115
+ return None
@@ -239,6 +239,10 @@ class PineconeConnection(VectorDBConnection):
239
239
  log_error("Embeddings are required for Pinecone and computing them failed: %s", e)
240
240
  return False
241
241
 
242
+ if not embeddings:
243
+ log_error("Embeddings are required for Pinecone but none were provided or computed")
244
+ return False
245
+
242
246
  index = self._get_index(collection_name)
243
247
  if not index:
244
248
  return False
@@ -512,6 +512,19 @@ class QdrantConnection(VectorDBConnection):
512
512
  """
513
513
  if not self._client:
514
514
  return False
515
+ # Reject empty document lists early
516
+ if not documents:
517
+ return False
518
+
519
+ # If embeddings provided, ensure counts match
520
+ if embeddings is not None and len(embeddings) != len(documents):
521
+ log_error(
522
+ "Embeddings length (%d) does not match documents length (%d) for collection %s",
523
+ len(embeddings),
524
+ len(documents),
525
+ collection_name,
526
+ )
527
+ return False
515
528
 
516
529
  # If embeddings not provided, compute using model resolution helper
517
530
  if not embeddings and documents:
@@ -0,0 +1,73 @@
1
+ import os
2
+ import json
3
+ import time
4
+ import requests
5
+ from typing import Optional, Dict
6
+
7
+ GITHUB_API_URL = "https://api.github.com/repos/anthonypdawson/vector-inspector/releases/latest"
8
+ CACHE_FILE = os.path.expanduser("~/.vector_inspector_update_cache.json")
9
+ CACHE_TTL = 24 * 60 * 60 # 1 day in seconds
10
+
11
+
12
+ class UpdateService:
13
+ @staticmethod
14
+ def get_latest_release(force_refresh: bool = False) -> Optional[Dict]:
15
+ """
16
+ Fetch the latest release info from GitHub, with caching and rate limit handling.
17
+ Returns None on error or if rate limited.
18
+ """
19
+ now = int(time.time())
20
+ # Check cache for rate limit state or valid release
21
+ if os.path.exists(CACHE_FILE):
22
+ try:
23
+ with open(CACHE_FILE, "r", encoding="utf-8") as f:
24
+ cache = json.load(f)
25
+ # If rate limited, respect the reset time
26
+ if cache.get("rate_limited_until", 0) > now:
27
+ return None
28
+ if not force_refresh and now - cache.get("timestamp", 0) < CACHE_TTL:
29
+ return cache.get("release")
30
+ except Exception:
31
+ pass
32
+ try:
33
+ resp = requests.get(GITHUB_API_URL, timeout=5)
34
+ if resp.status_code == 200:
35
+ release = resp.json()
36
+ with open(CACHE_FILE, "w", encoding="utf-8") as f:
37
+ json.dump({"timestamp": now, "release": release}, f)
38
+ return release
39
+ elif resp.status_code == 403:
40
+ # Check for rate limit headers
41
+ reset = resp.headers.get("X-RateLimit-Reset")
42
+ if reset:
43
+ rate_limited_until = int(reset)
44
+ else:
45
+ # Default to 1 hour if no header
46
+ rate_limited_until = now + 3600
47
+ with open(CACHE_FILE, "w", encoding="utf-8") as f:
48
+ json.dump({"rate_limited_until": rate_limited_until}, f)
49
+ return None
50
+ except Exception:
51
+ pass
52
+ return None
53
+
54
+ @staticmethod
55
+ def compare_versions(current_version: str, latest_version: str) -> bool:
56
+ """
57
+ Returns True if latest_version is newer than current_version.
58
+ """
59
+
60
+ def parse(v):
61
+ return [int(x) for x in v.strip("v").split(".") if x.isdigit()]
62
+
63
+ return parse(latest_version) > parse(current_version)
64
+
65
+ @staticmethod
66
+ def get_update_instructions() -> Dict[str, str]:
67
+ """
68
+ Returns update instructions for both PyPI and GitHub.
69
+ """
70
+ return {
71
+ "pip": "pip install --upgrade vector-inspector",
72
+ "github": "https://github.com/anthonypdawson/vector-inspector/releases/latest",
73
+ }
@@ -0,0 +1,47 @@
1
+ from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton, QTextEdit, QHBoxLayout
2
+ from PySide6.QtCore import Qt
3
+ from PySide6.QtGui import QDesktopServices, QCursor
4
+ from PySide6.QtCore import QUrl
5
+
6
+
7
+ class UpdateDetailsDialog(QDialog):
8
+ def __init__(
9
+ self, version: str, release_notes: str, pip_command: str, github_url: str, parent=None
10
+ ):
11
+ super().__init__(parent)
12
+ self.setWindowTitle(f"Update Available: v{version}")
13
+ self.setMinimumWidth(500)
14
+ layout = QVBoxLayout(self)
15
+
16
+ title = QLabel(f"<b>New version available: v{version}</b>")
17
+ title.setAlignment(Qt.AlignCenter)
18
+ layout.addWidget(title)
19
+
20
+ notes_label = QLabel("<b>Release Notes:</b>")
21
+ layout.addWidget(notes_label)
22
+
23
+ notes = QTextEdit()
24
+ notes.setReadOnly(True)
25
+ notes.setPlainText(release_notes)
26
+ layout.addWidget(notes)
27
+
28
+ # Add vertical space before update instructions
29
+ from PySide6.QtWidgets import QSpacerItem, QSizePolicy
30
+
31
+ layout.addSpacing(16)
32
+
33
+ pip_label = QLabel(f"<b>Update with pip:</b> <code>{pip_command}</code>")
34
+ layout.addWidget(pip_label)
35
+
36
+ btn_layout = QHBoxLayout()
37
+ github_btn = QPushButton("View on GitHub")
38
+ github_btn.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(github_url)))
39
+ btn_layout.addWidget(github_btn)
40
+
41
+ close_btn = QPushButton("Close")
42
+ close_btn.clicked.connect(self.accept)
43
+ btn_layout.addWidget(close_btn)
44
+
45
+ layout.addLayout(btn_layout)
46
+
47
+ self.setLayout(layout)
@@ -211,10 +211,28 @@ class MainWindow(QMainWindow):
211
211
 
212
212
  # Help menu
213
213
  help_menu = menubar.addMenu("&Help")
214
-
215
214
  about_action = QAction("&About", self)
216
215
  about_action.triggered.connect(self._show_about)
217
216
  help_menu.addAction(about_action)
217
+ check_update_action = QAction("Check for Update", self)
218
+ check_update_action.triggered.connect(self._check_for_update_from_menu)
219
+ help_menu.addAction(check_update_action)
220
+
221
+ def _check_for_update_from_menu(self):
222
+ from vector_inspector.services.update_service import UpdateService
223
+ from vector_inspector.utils.version import get_app_version
224
+ from PySide6.QtWidgets import QMessageBox
225
+
226
+ latest = UpdateService.get_latest_release(force_refresh=True)
227
+ if latest:
228
+ current_version = get_app_version()
229
+ latest_version = latest.get("tag_name")
230
+ if latest_version and UpdateService.compare_versions(current_version, latest_version):
231
+ # Show update modal
232
+ self._latest_release = latest
233
+ self._on_update_indicator_clicked(None)
234
+ return
235
+ QMessageBox.information(self, "Check for Update", "No update available.")
218
236
 
219
237
  def _setup_toolbar(self):
220
238
  """Setup application toolbar."""
@@ -233,7 +251,7 @@ class MainWindow(QMainWindow):
233
251
  toolbar.addAction(refresh_action)
234
252
 
235
253
  def _setup_statusbar(self):
236
- """Setup status bar with connection breadcrumb."""
254
+ """Setup status bar with connection breadcrumb and update indicator."""
237
255
  status_bar = QStatusBar()
238
256
  self.setStatusBar(status_bar)
239
257
 
@@ -241,8 +259,62 @@ class MainWindow(QMainWindow):
241
259
  self.breadcrumb_label = QLabel("No active connection")
242
260
  self.statusBar().addPermanentWidget(self.breadcrumb_label)
243
261
 
262
+ # Update indicator label (hidden by default)
263
+ self.update_indicator = QLabel()
264
+ self.update_indicator.setText("")
265
+ self.update_indicator.setStyleSheet(
266
+ "color: #2980b9; font-weight: bold; text-decoration: underline;"
267
+ )
268
+ self.update_indicator.setVisible(False)
269
+ self.update_indicator.setCursor(Qt.PointingHandCursor)
270
+ self.statusBar().addPermanentWidget(self.update_indicator)
271
+
244
272
  self.statusBar().showMessage("Ready")
245
273
 
274
+ # Connect click event
275
+ self.update_indicator.mousePressEvent = self._on_update_indicator_clicked
276
+
277
+ # Check for updates on launch
278
+ from vector_inspector.services.update_service import UpdateService
279
+ from vector_inspector.utils.version import get_app_version
280
+ import threading
281
+
282
+ from PySide6.QtCore import QTimer
283
+
284
+ def check_updates():
285
+ latest = UpdateService.get_latest_release()
286
+ if latest:
287
+ current_version = get_app_version()
288
+ latest_version = latest.get("tag_name")
289
+ if latest_version and UpdateService.compare_versions(
290
+ current_version, latest_version
291
+ ):
292
+
293
+ def show_update():
294
+ self._latest_release = latest
295
+ self.update_indicator.setText(f"Update available: v{latest_version}")
296
+ self.update_indicator.setVisible(True)
297
+
298
+ QTimer.singleShot(0, show_update)
299
+
300
+ threading.Thread(target=check_updates, daemon=True).start()
301
+
302
+ def _on_update_indicator_clicked(self, event):
303
+ # Show update details dialog
304
+ if not hasattr(self, "_latest_release"):
305
+ return
306
+ from vector_inspector.ui.components.update_details_dialog import UpdateDetailsDialog
307
+ from vector_inspector.services.update_service import UpdateService
308
+
309
+ latest = self._latest_release
310
+ version = latest.get("tag_name", "?")
311
+ notes = latest.get("body", "")
312
+ instructions = UpdateService.get_update_instructions()
313
+ pip_cmd = instructions["pip"]
314
+ github_url = instructions["github"]
315
+ dlg = UpdateDetailsDialog(version, notes, pip_cmd, github_url, self)
316
+ dlg.exec()
317
+
246
318
  def _connect_signals(self):
247
319
  """Connect signals between components."""
248
320
  # Connection manager signals
@@ -780,6 +780,78 @@ class MetadataView(QWidget):
780
780
  # compute its page and load that page while selecting the row. This
781
781
  # ensures the edited item becomes visible even if the backend moved it.
782
782
  try:
783
+ # Quick in-place update: if the updated item is still on the
784
+ # currently-visible page, update the in-memory page and
785
+ # table cells and emit `dataChanged` so the view refreshes
786
+ # immediately without a full reload.
787
+ updated_id = updated_data.get("id")
788
+ if (
789
+ self.current_data
790
+ and self.current_data.get("ids")
791
+ and updated_id in self.current_data.get("ids", [])
792
+ ):
793
+ try:
794
+ row_idx = self.current_data["ids"].index(updated_id)
795
+
796
+ # Update in-memory lists
797
+ if "documents" in self.current_data and row_idx < len(
798
+ self.current_data["documents"]
799
+ ):
800
+ self.current_data["documents"][row_idx] = (
801
+ updated_data["document"] if updated_data["document"] else ""
802
+ )
803
+ if "metadatas" in self.current_data and row_idx < len(
804
+ self.current_data["metadatas"]
805
+ ):
806
+ self.current_data["metadatas"][row_idx] = (
807
+ updated_data["metadata"] if updated_data["metadata"] else {}
808
+ )
809
+
810
+ # Update table cell text for document column
811
+ doc_text = (
812
+ str(self.current_data["documents"][row_idx])
813
+ if self.current_data["documents"][row_idx]
814
+ else ""
815
+ )
816
+ if len(doc_text) > 100:
817
+ doc_text = doc_text[:100] + "..."
818
+ self.table.setItem(row_idx, 1, QTableWidgetItem(doc_text))
819
+
820
+ # Update metadata columns based on current header names
821
+ metadata_keys = []
822
+ for col in range(2, self.table.columnCount()):
823
+ hdr = self.table.horizontalHeaderItem(col)
824
+ if hdr:
825
+ metadata_keys.append(hdr.text())
826
+
827
+ if "metadatas" in self.current_data:
828
+ meta = self.current_data["metadatas"][row_idx]
829
+ for col_idx, key in enumerate(metadata_keys, start=2):
830
+ value = meta.get(key, "")
831
+ self.table.setItem(
832
+ row_idx, col_idx, QTableWidgetItem(str(value))
833
+ )
834
+
835
+ # Emit dataChanged on the underlying model so views refresh
836
+ try:
837
+ model = self.table.model()
838
+ top = model.index(row_idx, 0)
839
+ bottom = model.index(row_idx, self.table.columnCount() - 1)
840
+ model.dataChanged.emit(top, bottom, [Qt.DisplayRole, Qt.EditRole])
841
+ except Exception:
842
+ pass
843
+
844
+ # Restore selection/scroll and return
845
+ self.table.verticalScrollBar().setValue(
846
+ self.table.verticalScrollBar().value()
847
+ )
848
+ self.table.selectRow(row_idx)
849
+ self.table.scrollToItem(self.table.item(row_idx, 0))
850
+ return
851
+ except Exception:
852
+ # Fall through to server-side search if in-place update fails
853
+ pass
854
+
783
855
  server_filter = None
784
856
  if self.filter_group.isChecked() and self.filter_builder.has_filters():
785
857
  server_filter, _ = self.filter_builder.get_filters_split()
@@ -1,5 +1,12 @@
1
- APP_VERSION = "0.3.3" # Update this when pyproject.toml version changes
2
-
3
-
4
1
  def get_app_version():
5
- return APP_VERSION
2
+ try:
3
+ from importlib.metadata import version, PackageNotFoundError
4
+ except ImportError:
5
+ try:
6
+ from importlib_metadata import version, PackageNotFoundError # type: ignore
7
+ except ImportError:
8
+ return "?"
9
+ try:
10
+ return version("vector-inspector")
11
+ except PackageNotFoundError:
12
+ return "?"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vector-inspector
3
- Version: 0.3.6
3
+ Version: 0.3.7
4
4
  Summary: A comprehensive desktop application for visualizing, querying, and managing vector database data
5
5
  Author-Email: Anthony Dawson <anthonypdawson+github@gmail.com>
6
6
  License: MIT
@@ -31,9 +31,13 @@ Requires-Dist: pgvector>=0.4.2
31
31
  Description-Content-Type: text/markdown
32
32
 
33
33
  # Latest updates
34
- - Added Postgres (pgvector extension) support as a new vector database connection option.
35
- - Fixed issue with embedding regeneration on add/edit
36
- - Added option for editing items without regenerating embeddings
34
+ * Added automatic update notification system:
35
+ - Checks for new releases on GitHub once per launch/day
36
+ - Shows update indicator in the status bar and Help menu
37
+ - Clickable indicator opens a modal with release notes and update instructions
38
+ - Manual Check for Update in Help menu
39
+ - Version detection now uses installed package metadata for accuracy
40
+ * Fixed bug where data browser row disappeared post-edit
37
41
  ---
38
42
 
39
43
  # Vector Inspector
@@ -1,6 +1,6 @@
1
- vector_inspector-0.3.6.dist-info/METADATA,sha256=p7H27JQIdMv3bNqDlU98WDzCVdWkNJTirylQTbGW8DQ,10587
2
- vector_inspector-0.3.6.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
3
- vector_inspector-0.3.6.dist-info/entry_points.txt,sha256=u96envMI2NFImZUJDFutiiWl7ZoHrrev9joAgtyvTxo,80
1
+ vector_inspector-0.3.7.dist-info/METADATA,sha256=X24i5_fg7PgJqWp5pUSR3EB5S2j7_UknoOKjPdYJaBs,10786
2
+ vector_inspector-0.3.7.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
3
+ vector_inspector-0.3.7.dist-info/entry_points.txt,sha256=u96envMI2NFImZUJDFutiiWl7ZoHrrev9joAgtyvTxo,80
4
4
  vector_inspector/__init__.py,sha256=Q8XbXn98o0eliQWPePhy-aGUz2KNnVg7bQq-sBPl7zQ,119
5
5
  vector_inspector/__main__.py,sha256=Vdhw8YA1K3wPMlbJQYL5WqvRzAKVeZ16mZQFO9VRmCo,62
6
6
  vector_inspector/config/__init__.py,sha256=vHkVsXSUdInsfzWSOLPZzaaELa3SGenAgfpY5EYbsYA,95
@@ -10,10 +10,10 @@ vector_inspector/core/cache_manager.py,sha256=cHdbIYR-eS9vLLTqvq4Xejyi5Z7Fm9DqMh
10
10
  vector_inspector/core/connection_manager.py,sha256=xNmgSXqJcMC-iaOY33-Xnfxq4QjUN4CuwppPjzhn3DY,9958
11
11
  vector_inspector/core/connections/__init__.py,sha256=lDZ-Qv-CbBvVcSlT8K2824zojovEIKhykHVSLARHZWs,345
12
12
  vector_inspector/core/connections/base_connection.py,sha256=jDA1cEeNbTghqcCZYoLRpRPXIUteU5mSdpKcjvr4JQI,12236
13
- vector_inspector/core/connections/chroma_connection.py,sha256=YE5kzW5poGqQqEvwVWx_E9_E3iPag8y6uGFSG0Mv9jY,20799
14
- vector_inspector/core/connections/pgvector_connection.py,sha256=rcx567u5vQxQv-v_lEoi34hmfHpoM7f3UTelatzSRCw,43688
15
- vector_inspector/core/connections/pinecone_connection.py,sha256=TbvPmbMygkqssb7w4HGc-h_PxI-KimIf1OL9bQ_fyLk,26804
16
- vector_inspector/core/connections/qdrant_connection.py,sha256=67SxKiHH3m3jxGIyHt7E6ZduNiQ93ROKLsbfag_h7-I,31478
13
+ vector_inspector/core/connections/chroma_connection.py,sha256=Mks17olaHIxvJAdCVB6EoQa5Tj7ScoD3_b2--Ra0Owk,21006
14
+ vector_inspector/core/connections/pgvector_connection.py,sha256=7xzS3WRaa8IMt-RMU056kzxqiu2SLPgYxDMz_Xw5LVc,43812
15
+ vector_inspector/core/connections/pinecone_connection.py,sha256=V6nHLB6DXfQJG8LiH1uPtWFs6otACkaNxeYrM0uY0AQ,26954
16
+ vector_inspector/core/connections/qdrant_connection.py,sha256=GkLmnwnswakSMRM16pmtjkR6QXBszDhvg-aKhmI9jxw,31958
17
17
  vector_inspector/core/connections/qdrant_helpers/__init__.py,sha256=u2YNjiW4sbNtqhTNOQr4hmOFqirlNlllsyBK2gWs9eU,262
18
18
  vector_inspector/core/connections/qdrant_helpers/qdrant_embedding_resolver.py,sha256=9lQrVnjIDH2YyMMZwHdAtcSeWhNwUDrmMkFik3xWvGU,1547
19
19
  vector_inspector/core/connections/qdrant_helpers/qdrant_filter_builder.py,sha256=WebqoWXuejaRUhpNTrEqf2JPG3BRQGhkz1HG82jBqLc,2514
@@ -35,6 +35,7 @@ vector_inspector/services/filter_service.py,sha256=xDrMxNWsYzRcR1n0Fd-yp6Fo-4aLb
35
35
  vector_inspector/services/import_export_service.py,sha256=4NOfAa6ZyvMyj5cDM4xu0Wqx0pgnK3cCNBGo3E6j4LE,10200
36
36
  vector_inspector/services/profile_service.py,sha256=AMeC6XOfI6Qumi0bKlTbqU-czMcle0rrHYK68ceK5r8,12856
37
37
  vector_inspector/services/settings_service.py,sha256=YfuPK4VIn7Kd36dpGsgzLAJw3fGybopDUcHhR30GSaY,7692
38
+ vector_inspector/services/update_service.py,sha256=B_l0qQUe0L_rYWNYnqvXET4UNf6bCSsl2LTkINtp0GE,2771
38
39
  vector_inspector/services/visualization_service.py,sha256=9TOK1S1u1U74wLpF5NdDiryyrjOzFnvE8kwjugf95Wk,4208
39
40
  vector_inspector/ui/__init__.py,sha256=262ZiXO6Luk8vZnhCIoYxOtGiny0bXK-BTKjxUNBx-w,43
40
41
  vector_inspector/ui/components/__init__.py,sha256=S-GWU1P820dJ6mHmeeBEy-CGF9fjpBeNf8vrbhRlFMk,30
@@ -45,19 +46,20 @@ vector_inspector/ui/components/item_dialog.py,sha256=VMwehEjQ6xrdxWygR9J-hHsLfzO
45
46
  vector_inspector/ui/components/loading_dialog.py,sha256=YEKYGU-R-Zz4CjXSArJtkNxgTy4O9hI5Bbt6qlIzD8U,1018
46
47
  vector_inspector/ui/components/profile_manager_panel.py,sha256=U-Ea6KC97ltj7bYtG4h9Okb97SbfBAvH1SusbYHTn1o,27930
47
48
  vector_inspector/ui/components/splash_window.py,sha256=lnCdva1fys0BQ1k_rEIQuWjQYXdhGZOf8zRweG2VdyM,1904
49
+ vector_inspector/ui/components/update_details_dialog.py,sha256=IQl4QZ30tHHOFay68qGOajQ6JuOB85plNoEazzMYS6k,1587
48
50
  vector_inspector/ui/dialogs/__init__.py,sha256=xtT77L91PFfm3zHYRENHkWHJaKPm1htuUzRXAF53P8w,211
49
51
  vector_inspector/ui/dialogs/cross_db_migration.py,sha256=BaUyic8l5Ywwql2hQyxVrCXHMjGtqerNAQHDYxcbQ54,15872
50
52
  vector_inspector/ui/dialogs/embedding_config_dialog.py,sha256=1K5LBSBXp590BvKwtHx9qgPwGREsn1mJ8cjFGSZHnMA,12926
51
53
  vector_inspector/ui/dialogs/provider_type_dialog.py,sha256=W_FAJuvicwBUJJ7PyvKow9lc8_a5pnE3RIAsh-DVndQ,6809
52
- vector_inspector/ui/main_window.py,sha256=UFWQt-yCTJhdS05bHX5bZfw_Bv4iAw1GavBc-mNaug4,27842
54
+ vector_inspector/ui/main_window.py,sha256=ma7QPSXom3mw9-nwJcOvuQxj_PoVcmM-4daSJEKKZVs,31055
53
55
  vector_inspector/ui/views/__init__.py,sha256=FeMtVzSbVFBMjdwLQSQqD0FRW4ieJ4ZKXtTBci2e_bw,30
54
56
  vector_inspector/ui/views/collection_browser.py,sha256=oG9_YGPoVuMs-f_zSd4EcITmEU9caxvwuubsFUrNf-c,3991
55
57
  vector_inspector/ui/views/connection_view.py,sha256=3oGbClqwpVuUD3AIT8TuM-8heDvwMYw7RowHT3b1b8o,23749
56
58
  vector_inspector/ui/views/info_panel.py,sha256=6LwOQFmRdFOcNZo_BcZ8DZ5a5GTdGLwO2IIHqYWNBdQ,26179
57
- vector_inspector/ui/views/metadata_view.py,sha256=WV21oTB8MV1CvMCyQ1T5zEM1OFCoMPEXxV0L-r0d7VE,41771
59
+ vector_inspector/ui/views/metadata_view.py,sha256=75ITKS-DPw1GVUBWI1ppTbADb5Ka4d82wPhM7iFxDGo,45776
58
60
  vector_inspector/ui/views/search_view.py,sha256=LFGmbyVoB04bE6KX-Gt-WYmFrFfnMy808iHWiWVH-gU,12291
59
61
  vector_inspector/ui/views/visualization_view.py,sha256=wgkSkOM-ShOHDj1GCUtKnqH87Io5vYtiOdubGV5rN44,11050
60
62
  vector_inspector/utils/__init__.py,sha256=jhHBQC8C8bfhNlf6CAt07ejjStp_YAyleaYr2dm0Dk0,38
61
63
  vector_inspector/utils/lazy_imports.py,sha256=2XZ3ZnwTvZ5vvrh36nJ_TUjwwkgjoAED6i6P9yctvt0,1211
62
- vector_inspector/utils/version.py,sha256=wFhZKqblnXu2pyXkg9pCOsUhFNOgTxEKS5fQZxXDQYk,121
63
- vector_inspector-0.3.6.dist-info/RECORD,,
64
+ vector_inspector/utils/version.py,sha256=2Xk9DEKlDRGEszNNiYnK7ps1i3OH56H2uZhR0_yZORs,382
65
+ vector_inspector-0.3.7.dist-info/RECORD,,