vector-inspector 0.3.5__tar.gz → 0.3.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/PKG-INFO +8 -4
  2. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/README.md +7 -3
  3. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/pyproject.toml +1 -1
  4. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/core/connections/chroma_connection.py +4 -1
  5. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/core/connections/pgvector_connection.py +108 -93
  6. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/core/connections/pinecone_connection.py +4 -0
  7. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/core/connections/qdrant_connection.py +13 -0
  8. vector_inspector-0.3.7/src/vector_inspector/services/update_service.py +73 -0
  9. vector_inspector-0.3.7/src/vector_inspector/ui/components/update_details_dialog.py +47 -0
  10. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/ui/main_window.py +74 -2
  11. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/ui/views/metadata_view.py +72 -0
  12. vector_inspector-0.3.7/src/vector_inspector/utils/version.py +12 -0
  13. vector_inspector-0.3.7/tests/test_chroma_connection.py +105 -0
  14. vector_inspector-0.3.7/tests/test_pgvector_connection.py +91 -0
  15. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/tests/test_pinecone_connection.py +114 -98
  16. vector_inspector-0.3.7/tests/test_qdrant_connection.py +128 -0
  17. vector_inspector-0.3.5/src/vector_inspector/utils/version.py +0 -5
  18. vector_inspector-0.3.5/tests/test_connections.py +0 -71
  19. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/__init__.py +0 -0
  20. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/__main__.py +0 -0
  21. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/config/__init__.py +0 -0
  22. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/config/known_embedding_models.json +0 -0
  23. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/core/__init__.py +0 -0
  24. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/core/cache_manager.py +0 -0
  25. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/core/connection_manager.py +0 -0
  26. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/core/connections/__init__.py +0 -0
  27. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/core/connections/base_connection.py +0 -0
  28. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/core/connections/qdrant_helpers/__init__.py +0 -0
  29. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/core/connections/qdrant_helpers/qdrant_embedding_resolver.py +0 -0
  30. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/core/connections/qdrant_helpers/qdrant_filter_builder.py +0 -0
  31. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/core/connections/template_connection.py +0 -0
  32. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/core/embedding_providers/__init__.py +0 -0
  33. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/core/embedding_providers/base_provider.py +0 -0
  34. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/core/embedding_providers/clip_provider.py +0 -0
  35. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/core/embedding_providers/provider_factory.py +0 -0
  36. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/core/embedding_providers/sentence_transformer_provider.py +0 -0
  37. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/core/embedding_utils.py +0 -0
  38. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/core/logging.py +0 -0
  39. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/core/model_registry.py +0 -0
  40. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/main.py +0 -0
  41. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/services/__init__.py +0 -0
  42. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/services/backup_helpers.py +0 -0
  43. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/services/backup_restore_service.py +0 -0
  44. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/services/credential_service.py +0 -0
  45. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/services/filter_service.py +0 -0
  46. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/services/import_export_service.py +0 -0
  47. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/services/profile_service.py +0 -0
  48. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/services/settings_service.py +0 -0
  49. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/services/visualization_service.py +0 -0
  50. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/ui/__init__.py +0 -0
  51. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/ui/components/__init__.py +0 -0
  52. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/ui/components/backup_restore_dialog.py +0 -0
  53. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/ui/components/connection_manager_panel.py +0 -0
  54. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/ui/components/filter_builder.py +0 -0
  55. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/ui/components/item_dialog.py +0 -0
  56. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/ui/components/loading_dialog.py +0 -0
  57. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/ui/components/profile_manager_panel.py +0 -0
  58. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/ui/components/splash_window.py +0 -0
  59. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/ui/dialogs/__init__.py +0 -0
  60. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/ui/dialogs/cross_db_migration.py +0 -0
  61. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/ui/dialogs/embedding_config_dialog.py +0 -0
  62. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/ui/dialogs/provider_type_dialog.py +0 -0
  63. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/ui/views/__init__.py +0 -0
  64. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/ui/views/collection_browser.py +0 -0
  65. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/ui/views/connection_view.py +0 -0
  66. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/ui/views/info_panel.py +0 -0
  67. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/ui/views/search_view.py +0 -0
  68. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/ui/views/visualization_view.py +0 -0
  69. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/utils/__init__.py +0 -0
  70. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/src/vector_inspector/utils/lazy_imports.py +0 -0
  71. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/tests/test_backup_helpers.py +0 -0
  72. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/tests/test_backup_restore_service.py +0 -0
  73. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/tests/test_filter_service.py +0 -0
  74. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/tests/test_runner.py +0 -0
  75. {vector_inspector-0.3.5 → vector_inspector-0.3.7}/tests/test_settings_service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vector-inspector
3
- Version: 0.3.5
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 and tested support for python 3.13
35
- - Miscellaneous documentation updates and fixes
36
- - Splash screen now shows after main window is visible
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,7 +1,11 @@
1
1
  # Latest updates
2
- - Added and tested support for python 3.13
3
- - Miscellaneous documentation updates and fixes
4
- - Splash screen now shows after main window is visible
2
+ * Added automatic update notification system:
3
+ - Checks for new releases on GitHub once per launch/day
4
+ - Shows update indicator in the status bar and Help menu
5
+ - Clickable indicator opens a modal with release notes and update instructions
6
+ - Manual Check for Update in Help menu
7
+ - Version detection now uses installed package metadata for accuracy
8
+ * Fixed bug where data browser row disappeared post-edit
5
9
  ---
6
10
 
7
11
  # Vector Inspector
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "vector-inspector"
3
- version = "0.3.5"
3
+ version = "0.3.7"
4
4
  description = "A comprehensive desktop application for visualizing, querying, and managing vector database data"
5
5
  authors = [
6
6
  { name = "Anthony Dawson", email = "anthonypdawson+github@gmail.com" },
@@ -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