vector-inspector 0.3.2__py3-none-any.whl → 0.3.4__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.
Files changed (32) hide show
  1. vector_inspector/core/connection_manager.py +55 -49
  2. vector_inspector/core/connections/base_connection.py +41 -41
  3. vector_inspector/core/connections/chroma_connection.py +110 -86
  4. vector_inspector/core/connections/pinecone_connection.py +168 -182
  5. vector_inspector/core/connections/qdrant_connection.py +109 -126
  6. vector_inspector/core/connections/qdrant_helpers/__init__.py +4 -0
  7. vector_inspector/core/connections/qdrant_helpers/qdrant_embedding_resolver.py +35 -0
  8. vector_inspector/core/connections/qdrant_helpers/qdrant_filter_builder.py +51 -0
  9. vector_inspector/core/connections/template_connection.py +55 -65
  10. vector_inspector/core/embedding_utils.py +32 -32
  11. vector_inspector/core/logging.py +27 -0
  12. vector_inspector/core/model_registry.py +4 -3
  13. vector_inspector/main.py +6 -2
  14. vector_inspector/services/backup_helpers.py +63 -0
  15. vector_inspector/services/backup_restore_service.py +73 -152
  16. vector_inspector/services/credential_service.py +33 -40
  17. vector_inspector/services/import_export_service.py +70 -67
  18. vector_inspector/services/profile_service.py +92 -94
  19. vector_inspector/services/settings_service.py +68 -48
  20. vector_inspector/services/visualization_service.py +40 -39
  21. vector_inspector/ui/components/splash_window.py +57 -0
  22. vector_inspector/ui/dialogs/cross_db_migration.py +6 -5
  23. vector_inspector/ui/main_window.py +200 -146
  24. vector_inspector/ui/views/info_panel.py +208 -127
  25. vector_inspector/ui/views/metadata_view.py +8 -7
  26. vector_inspector/ui/views/search_view.py +97 -75
  27. vector_inspector/ui/views/visualization_view.py +140 -97
  28. vector_inspector/utils/version.py +5 -0
  29. {vector_inspector-0.3.2.dist-info → vector_inspector-0.3.4.dist-info}/METADATA +10 -2
  30. {vector_inspector-0.3.2.dist-info → vector_inspector-0.3.4.dist-info}/RECORD +32 -25
  31. {vector_inspector-0.3.2.dist-info → vector_inspector-0.3.4.dist-info}/WHEEL +0 -0
  32. {vector_inspector-0.3.2.dist-info → vector_inspector-0.3.4.dist-info}/entry_points.txt +0 -0
@@ -4,67 +4,69 @@ import json
4
4
  from pathlib import Path
5
5
  from typing import Dict, Any, Optional, List
6
6
  from vector_inspector.core.cache_manager import invalidate_cache_on_settings_change
7
+ from vector_inspector.core.logging import log_error
7
8
 
8
9
 
9
10
  class SettingsService:
10
11
  """Handles loading and saving application settings."""
11
-
12
+
12
13
  def __init__(self):
13
14
  """Initialize settings service."""
14
15
  self.settings_dir = Path.home() / ".vector-inspector"
15
16
  self.settings_file = self.settings_dir / "settings.json"
16
17
  self.settings: Dict[str, Any] = {}
17
18
  self._load_settings()
18
-
19
+
19
20
  def _load_settings(self):
20
21
  """Load settings from file."""
21
22
  try:
22
23
  if self.settings_file.exists():
23
- with open(self.settings_file, 'r', encoding='utf-8') as f:
24
+ with open(self.settings_file, "r", encoding="utf-8") as f:
24
25
  self.settings = json.load(f)
25
26
  except Exception as e:
26
- print(f"Failed to load settings: {e}")
27
+ log_error("Failed to load settings: %s", e)
27
28
  self.settings = {}
28
-
29
+
29
30
  def _save_settings(self):
30
31
  """Save settings to file."""
31
32
  try:
32
33
  # Create settings directory if it doesn't exist
33
34
  self.settings_dir.mkdir(parents=True, exist_ok=True)
34
-
35
- with open(self.settings_file, 'w', encoding='utf-8') as f:
35
+
36
+ with open(self.settings_file, "w", encoding="utf-8") as f:
36
37
  json.dump(self.settings, f, indent=2, ensure_ascii=False)
37
38
  except Exception as e:
38
- print(f"Failed to save settings: {e}")
39
-
39
+ log_error("Failed to save settings: %s", e)
40
+
40
41
  def get_last_connection(self) -> Optional[Dict[str, Any]]:
41
42
  """Get the last connection configuration."""
42
43
  return self.settings.get("last_connection")
43
-
44
+
44
45
  def save_last_connection(self, config: Dict[str, Any]):
45
46
  """Save the last connection configuration."""
46
47
  self.settings["last_connection"] = config
47
48
  self._save_settings()
48
-
49
+
49
50
  def get(self, key: str, default: Any = None) -> Any:
50
51
  """Get a setting value."""
51
52
  return self.settings.get(key, default)
52
-
53
+
53
54
  def get_cache_enabled(self) -> bool:
54
55
  """Get whether caching is enabled (default: True)."""
55
56
  return self.settings.get("cache_enabled", True)
56
-
57
+
57
58
  def set_cache_enabled(self, enabled: bool):
58
59
  """Set whether caching is enabled."""
59
60
  self.set("cache_enabled", enabled)
60
61
  # Update cache manager state
61
62
  from vector_inspector.core.cache_manager import get_cache_manager
63
+
62
64
  cache = get_cache_manager()
63
65
  if enabled:
64
66
  cache.enable()
65
67
  else:
66
68
  cache.disable()
67
-
69
+
68
70
  def set(self, key: str, value: Any):
69
71
  """Set a setting value."""
70
72
  self.settings[key] = value
@@ -72,15 +74,21 @@ class SettingsService:
72
74
  # Invalidate cache when settings change (only if cache is enabled)
73
75
  if key != "cache_enabled": # Don't invalidate when toggling cache itself
74
76
  invalidate_cache_on_settings_change()
75
-
77
+
76
78
  def clear(self):
77
79
  """Clear all settings."""
78
80
  self.settings = {}
79
81
  self._save_settings()
80
-
81
- def save_embedding_model(self, connection_id: str, collection_name: str, model_name: str, model_type: str = "user-configured"):
82
+
83
+ def save_embedding_model(
84
+ self,
85
+ connection_id: str,
86
+ collection_name: str,
87
+ model_name: str,
88
+ model_type: str = "user-configured",
89
+ ):
82
90
  """Save embedding model mapping for a collection.
83
-
91
+
84
92
  Args:
85
93
  connection_id: Connection identifier
86
94
  collection_name: Collection name
@@ -89,51 +97,60 @@ class SettingsService:
89
97
  """
90
98
  if "collection_embedding_models" not in self.settings:
91
99
  self.settings["collection_embedding_models"] = {}
92
-
100
+
93
101
  collection_key = f"{connection_id}:{collection_name}"
94
102
  self.settings["collection_embedding_models"][collection_key] = {
95
103
  "model": model_name,
96
104
  "type": model_type,
97
- "timestamp": self._get_timestamp()
105
+ "timestamp": self._get_timestamp(),
98
106
  }
99
107
  self._save_settings()
100
-
101
- def get_embedding_model(self, connection_id: str, collection_name: str) -> Optional[Dict[str, Any]]:
108
+
109
+ def get_embedding_model(
110
+ self, connection_id: str, collection_name: str
111
+ ) -> Optional[Dict[str, Any]]:
102
112
  """Get embedding model mapping for a collection.
103
-
113
+
104
114
  Args:
105
115
  connection_id: Connection identifier
106
116
  collection_name: Collection name
107
-
117
+
108
118
  Returns:
109
119
  Dictionary with 'model', 'type', and 'timestamp' or None
110
120
  """
111
121
  collection_models = self.settings.get("collection_embedding_models", {})
112
122
  collection_key = f"{connection_id}:{collection_name}"
113
123
  return collection_models.get(collection_key)
114
-
124
+
115
125
  def remove_embedding_model(self, connection_id: str, collection_name: str):
116
126
  """Remove embedding model mapping for a collection.
117
-
127
+
118
128
  Args:
119
129
  connection_id: Connection identifier
120
130
  collection_name: Collection name
121
131
  """
122
132
  if "collection_embedding_models" not in self.settings:
123
133
  return
124
-
134
+
125
135
  collection_key = f"{connection_id}:{collection_name}"
126
136
  self.settings["collection_embedding_models"].pop(collection_key, None)
127
137
  self._save_settings()
128
-
138
+
129
139
  def _get_timestamp(self) -> str:
130
140
  """Get current timestamp as ISO string."""
131
141
  from datetime import datetime
142
+
132
143
  return datetime.now().isoformat()
133
-
134
- def add_custom_embedding_model(self, model_name: str, dimension: int, model_type: str = "sentence-transformer", description: str = "Custom model"):
144
+
145
+ def add_custom_embedding_model(
146
+ self,
147
+ model_name: str,
148
+ dimension: int,
149
+ model_type: str = "sentence-transformer",
150
+ description: str = "Custom model",
151
+ ):
135
152
  """Add a custom embedding model to the known models list.
136
-
153
+
137
154
  Args:
138
155
  model_name: Name of the embedding model
139
156
  dimension: Vector dimension
@@ -142,7 +159,7 @@ class SettingsService:
142
159
  """
143
160
  if "custom_embedding_models" not in self.settings:
144
161
  self.settings["custom_embedding_models"] = []
145
-
162
+
146
163
  # Check if already exists
147
164
  for model in self.settings["custom_embedding_models"]:
148
165
  if model["name"] == model_name and model["dimension"] == dimension:
@@ -152,24 +169,26 @@ class SettingsService:
152
169
  model["last_used"] = self._get_timestamp()
153
170
  self._save_settings()
154
171
  return
155
-
172
+
156
173
  # Add new entry
157
- self.settings["custom_embedding_models"].append({
158
- "name": model_name,
159
- "dimension": dimension,
160
- "type": model_type,
161
- "description": description,
162
- "added": self._get_timestamp(),
163
- "last_used": self._get_timestamp()
164
- })
174
+ self.settings["custom_embedding_models"].append(
175
+ {
176
+ "name": model_name,
177
+ "dimension": dimension,
178
+ "type": model_type,
179
+ "description": description,
180
+ "added": self._get_timestamp(),
181
+ "last_used": self._get_timestamp(),
182
+ }
183
+ )
165
184
  self._save_settings()
166
-
185
+
167
186
  def get_custom_embedding_models(self, dimension: Optional[int] = None) -> List[Dict[str, Any]]:
168
187
  """Get list of custom embedding models.
169
-
188
+
170
189
  Args:
171
190
  dimension: Optional filter by dimension
172
-
191
+
173
192
  Returns:
174
193
  List of custom model dictionaries
175
194
  """
@@ -177,19 +196,20 @@ class SettingsService:
177
196
  if dimension is not None:
178
197
  return [m for m in models if m["dimension"] == dimension]
179
198
  return models
180
-
199
+
181
200
  def remove_custom_embedding_model(self, model_name: str, dimension: int):
182
201
  """Remove a custom embedding model.
183
-
202
+
184
203
  Args:
185
204
  model_name: Name of the model to remove
186
205
  dimension: Vector dimension
187
206
  """
188
207
  if "custom_embedding_models" not in self.settings:
189
208
  return
190
-
209
+
191
210
  self.settings["custom_embedding_models"] = [
192
- m for m in self.settings["custom_embedding_models"]
211
+ m
212
+ for m in self.settings["custom_embedding_models"]
193
213
  if not (m["name"] == model_name and m["dimension"] == dimension)
194
214
  ]
195
215
  self._save_settings()
@@ -2,103 +2,96 @@
2
2
 
3
3
  from typing import Optional, List, Tuple, Any
4
4
  import warnings
5
+ from vector_inspector.core.logging import log_error
5
6
 
6
7
 
7
8
  class VisualizationService:
8
9
  """Service for vector dimensionality reduction and visualization."""
9
-
10
+
10
11
  @staticmethod
11
12
  def reduce_dimensions(
12
- embeddings: List[List[float]],
13
- method: str = "pca",
14
- n_components: int = 2,
15
- **kwargs
13
+ embeddings: List[List[float]], method: str = "pca", n_components: int = 2, **kwargs
16
14
  ) -> Optional[Any]:
17
15
  """
18
16
  Reduce dimensionality of embeddings.
19
-
17
+
20
18
  Args:
21
19
  embeddings: List of embedding vectors
22
20
  method: Reduction method ('pca', 'tsne', or 'umap')
23
21
  n_components: Target number of dimensions (2 or 3)
24
22
  **kwargs: Additional method-specific parameters
25
-
23
+
26
24
  Returns:
27
25
  Reduced embeddings as numpy array, or None if failed
28
26
  """
29
27
  if embeddings is None or len(embeddings) == 0:
30
28
  return None
31
-
29
+
32
30
  try:
33
31
  # Lazy import numpy and models
34
32
  from vector_inspector.utils.lazy_imports import get_numpy, get_sklearn_model
33
+
35
34
  np = get_numpy()
36
-
35
+
37
36
  X = np.array(embeddings)
38
-
37
+
39
38
  if method.lower() == "pca":
40
- PCA = get_sklearn_model('PCA')
39
+ PCA = get_sklearn_model("PCA")
41
40
  reducer = PCA(n_components=n_components)
42
41
  reduced = reducer.fit_transform(X)
43
-
42
+
44
43
  elif method.lower() in ["t-sne", "tsne"]:
45
- TSNE = get_sklearn_model('TSNE')
44
+ TSNE = get_sklearn_model("TSNE")
46
45
  perplexity = kwargs.get("perplexity", min(30, len(embeddings) - 1))
47
- reducer = TSNE(
48
- n_components=n_components,
49
- perplexity=perplexity,
50
- random_state=42
51
- )
46
+ reducer = TSNE(n_components=n_components, perplexity=perplexity, random_state=42)
52
47
  reduced = reducer.fit_transform(X)
53
-
48
+
54
49
  elif method.lower() == "umap":
55
- UMAP = get_sklearn_model('UMAP')
50
+ UMAP = get_sklearn_model("UMAP")
56
51
  n_neighbors = kwargs.get("n_neighbors", min(15, len(embeddings) - 1))
57
52
  # Suppress n_jobs warning when using random_state
58
53
  with warnings.catch_warnings():
59
54
  warnings.filterwarnings("ignore", message=".*n_jobs.*overridden.*")
60
55
  reducer = UMAP(
61
- n_components=n_components,
62
- n_neighbors=n_neighbors,
63
- random_state=42
56
+ n_components=n_components, n_neighbors=n_neighbors, random_state=42
64
57
  )
65
58
  reduced = reducer.fit_transform(X)
66
-
59
+
67
60
  else:
68
- print(f"Unknown method: {method}")
61
+ log_error("Unknown method: %s", method)
69
62
  return None
70
-
63
+
71
64
  return reduced
72
-
65
+
73
66
  except Exception as e:
74
- print(f"Dimensionality reduction failed: {e}")
67
+ log_error("Dimensionality reduction failed: %s", e)
75
68
  return None
76
-
69
+
77
70
  @staticmethod
78
71
  def prepare_plot_data(
79
72
  reduced_embeddings: Any,
80
73
  labels: Optional[List[str]] = None,
81
74
  metadata: Optional[List[dict]] = None,
82
- color_by: Optional[str] = None
75
+ color_by: Optional[str] = None,
83
76
  ) -> Tuple[Any, List[str], List[str]]:
84
77
  """
85
78
  Prepare data for plotting.
86
-
79
+
87
80
  Args:
88
81
  reduced_embeddings: Reduced dimension embeddings
89
82
  labels: Text labels for each point
90
83
  metadata: Metadata dictionaries for each point
91
84
  color_by: Metadata field to use for coloring
92
-
85
+
93
86
  Returns:
94
87
  Tuple of (embeddings, labels, colors)
95
88
  """
96
89
  n_points = len(reduced_embeddings)
97
-
90
+
98
91
  # Prepare labels
99
92
  if labels is None:
100
93
  labels = [f"Point {i}" for i in range(n_points)]
101
-
94
+
102
95
  # Prepare colors
103
96
  colors = ["blue"] * n_points
104
97
  if color_by and metadata:
@@ -108,16 +101,24 @@ class VisualizationService:
108
101
  value = meta.get(color_by, "unknown")
109
102
  values.append(str(value))
110
103
  unique_values.add(str(value))
111
-
104
+
112
105
  # Map values to colors
113
106
  color_map = {}
114
107
  color_palette = [
115
- "red", "blue", "green", "orange", "purple",
116
- "cyan", "magenta", "yellow", "pink", "brown"
108
+ "red",
109
+ "blue",
110
+ "green",
111
+ "orange",
112
+ "purple",
113
+ "cyan",
114
+ "magenta",
115
+ "yellow",
116
+ "pink",
117
+ "brown",
117
118
  ]
118
119
  for i, val in enumerate(sorted(unique_values)):
119
120
  color_map[val] = color_palette[i % len(color_palette)]
120
-
121
+
121
122
  colors = [color_map[val] for val in values]
122
-
123
+
123
124
  return reduced_embeddings, labels, colors
@@ -0,0 +1,57 @@
1
+ from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QCheckBox, QPushButton, QTextBrowser
2
+ from PySide6.QtCore import Qt
3
+
4
+
5
+ class SplashWindow(QDialog):
6
+ def __init__(self, parent=None):
7
+ super().__init__(parent)
8
+ self.setWindowTitle("Welcome to Vector Inspector!")
9
+ self.setModal(True)
10
+ self.setMinimumWidth(420)
11
+
12
+ layout = QVBoxLayout(self)
13
+
14
+ # Welcome message
15
+ label = QLabel("<h2>Thanks for trying <b>Vector Inspector</b>!</h2>")
16
+ label.setAlignment(Qt.AlignCenter)
17
+ layout.addWidget(label)
18
+
19
+ # Feedback prompt
20
+ feedback = QLabel(
21
+ "If you have thoughts, feature requests, or run into anything confusing,<br>"
22
+ "I’d really appreciate hearing from you. Feedback helps shape the roadmap."
23
+ )
24
+ feedback.setTextFormat(Qt.RichText)
25
+ feedback.setWordWrap(True)
26
+ layout.addWidget(feedback)
27
+
28
+ # GitHub link
29
+ github = QLabel(
30
+ '<a href="https://github.com/anthonypdawson/vector-inspector/issues">Submit feedback or issues on GitHub</a>'
31
+ )
32
+ github.setOpenExternalLinks(True)
33
+ github.setAlignment(Qt.AlignCenter)
34
+ layout.addWidget(github)
35
+
36
+ # About info (reuse About dialog text)
37
+ from vector_inspector.ui.main_window import get_about_html
38
+
39
+ about = QTextBrowser()
40
+ about.setHtml(get_about_html())
41
+ about.setOpenExternalLinks(True)
42
+ about.setMaximumHeight(160)
43
+ layout.addWidget(about)
44
+
45
+ # Do not show again checkbox
46
+ self.hide_checkbox = QCheckBox("Do not show this again")
47
+ layout.addWidget(self.hide_checkbox)
48
+
49
+ # OK button
50
+ ok_btn = QPushButton("OK")
51
+ ok_btn.clicked.connect(self.accept)
52
+ layout.addWidget(ok_btn)
53
+
54
+ layout.addStretch(1)
55
+
56
+ def should_hide(self):
57
+ return self.hide_checkbox.isChecked()
@@ -12,6 +12,7 @@ from PySide6.QtCore import QThread, Signal
12
12
 
13
13
  from vector_inspector.core.connection_manager import ConnectionManager, ConnectionInstance
14
14
  from vector_inspector.services.backup_restore_service import BackupRestoreService
15
+ from vector_inspector.core.logging import log_info, log_error
15
16
 
16
17
 
17
18
  class MigrationThread(QThread):
@@ -111,26 +112,26 @@ class MigrationThread(QThread):
111
112
  try:
112
113
  if self.target_collection in self.target_conn.connection.list_collections():
113
114
  self.progress.emit(90, "Cleaning up failed migration...")
114
- print(f"Cleaning up failed migration: deleting target collection '{self.target_collection}'")
115
+ log_info("Cleaning up failed migration: deleting target collection '%s'", self.target_collection)
115
116
  self.target_conn.connection.delete_collection(self.target_collection)
116
117
  except Exception as cleanup_error:
117
- print(f"Warning: Failed to clean up target collection: {cleanup_error}")
118
+ log_error("Warning: Failed to clean up target collection: %s", cleanup_error)
118
119
 
119
120
  self.finished.emit(False, "Failed to restore to target collection. Target collection cleaned up.")
120
121
 
121
122
  except Exception as e:
122
123
  import traceback
123
124
  error_details = traceback.format_exc()
124
- print(f"Migration error details:\n{error_details}")
125
+ log_error("Migration error details:\n%s", error_details)
125
126
 
126
127
  # Clean up target collection on exception
127
128
  try:
128
129
  if self.target_conn and self.target_conn.connection.is_connected:
129
130
  if self.target_collection in self.target_conn.connection.list_collections():
130
- print(f"Cleaning up failed migration: deleting target collection '{self.target_collection}'")
131
+ log_info("Cleaning up failed migration: deleting target collection '%s'", self.target_collection)
131
132
  self.target_conn.connection.delete_collection(self.target_collection)
132
133
  except Exception as cleanup_error:
133
- print(f"Warning: Failed to clean up target collection: {cleanup_error}")
134
+ log_error("Warning: Failed to clean up target collection: %s", cleanup_error)
134
135
 
135
136
  self.finished.emit(False, f"Migration error: {str(e)}")
136
137