vector-inspector 0.2.0__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/__init__.py +3 -0
  2. vector_inspector/__main__.py +4 -0
  3. vector_inspector/core/__init__.py +1 -0
  4. vector_inspector/core/connections/__init__.py +7 -0
  5. vector_inspector/core/connections/base_connection.py +233 -0
  6. vector_inspector/core/connections/chroma_connection.py +384 -0
  7. vector_inspector/core/connections/qdrant_connection.py +723 -0
  8. vector_inspector/core/connections/template_connection.py +346 -0
  9. vector_inspector/main.py +21 -0
  10. vector_inspector/services/__init__.py +1 -0
  11. vector_inspector/services/backup_restore_service.py +286 -0
  12. vector_inspector/services/filter_service.py +72 -0
  13. vector_inspector/services/import_export_service.py +287 -0
  14. vector_inspector/services/settings_service.py +60 -0
  15. vector_inspector/services/visualization_service.py +116 -0
  16. vector_inspector/ui/__init__.py +1 -0
  17. vector_inspector/ui/components/__init__.py +1 -0
  18. vector_inspector/ui/components/backup_restore_dialog.py +350 -0
  19. vector_inspector/ui/components/filter_builder.py +370 -0
  20. vector_inspector/ui/components/item_dialog.py +118 -0
  21. vector_inspector/ui/components/loading_dialog.py +30 -0
  22. vector_inspector/ui/main_window.py +288 -0
  23. vector_inspector/ui/views/__init__.py +1 -0
  24. vector_inspector/ui/views/collection_browser.py +112 -0
  25. vector_inspector/ui/views/connection_view.py +423 -0
  26. vector_inspector/ui/views/metadata_view.py +555 -0
  27. vector_inspector/ui/views/search_view.py +268 -0
  28. vector_inspector/ui/views/visualization_view.py +245 -0
  29. vector_inspector-0.2.0.dist-info/METADATA +382 -0
  30. vector_inspector-0.2.0.dist-info/RECORD +32 -0
  31. vector_inspector-0.2.0.dist-info/WHEEL +4 -0
  32. vector_inspector-0.2.0.dist-info/entry_points.txt +5 -0
@@ -0,0 +1,370 @@
1
+ """Advanced metadata filter builder component."""
2
+
3
+ from typing import Dict, Any, List, Optional
4
+ from PySide6.QtWidgets import (
5
+ QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox,
6
+ QLineEdit, QGroupBox, QScrollArea, QLabel, QFrame
7
+ )
8
+ from PySide6.QtCore import Signal, Qt
9
+ import json
10
+
11
+
12
+ class FilterRule(QWidget):
13
+ """A single filter rule widget."""
14
+
15
+ remove_requested = Signal(object) # Signal to remove this rule
16
+ apply_requested = Signal() # Signal to apply filters
17
+
18
+ def __init__(self, parent=None):
19
+ super().__init__(parent)
20
+ self._setup_ui()
21
+
22
+ def _setup_ui(self):
23
+ """Setup the rule UI."""
24
+ layout = QHBoxLayout(self)
25
+ layout.setContentsMargins(5, 5, 5, 5)
26
+
27
+ # Field name
28
+ self.field_input = QComboBox()
29
+ self.field_input.setEditable(True)
30
+ self.field_input.setPlaceholderText("field name")
31
+ self.field_input.setMinimumWidth(120)
32
+ layout.addWidget(self.field_input)
33
+
34
+ # Operator
35
+ self.operator_combo = QComboBox()
36
+ self.operator_combo.addItems([
37
+ "=",
38
+ "!=",
39
+ ">",
40
+ ">=",
41
+ "<",
42
+ "<=",
43
+ "in",
44
+ "not in",
45
+ "contains (client-side)",
46
+ "not contains (client-side)"
47
+ ])
48
+ self.operator_combo.setMinimumWidth(150)
49
+ self.operator_combo.setMinimumWidth(100)
50
+ layout.addWidget(self.operator_combo)
51
+
52
+ # Value
53
+ self.value_input = QLineEdit()
54
+ self.value_input.setPlaceholderText("value")
55
+ self.value_input.setMinimumWidth(150)
56
+ # Apply filters on Enter key or when clicking away
57
+ self.value_input.returnPressed.connect(self.apply_requested.emit)
58
+ self.value_input.editingFinished.connect(self.apply_requested.emit)
59
+ layout.addWidget(self.value_input)
60
+
61
+ # Remove button
62
+ remove_btn = QPushButton("✕")
63
+ remove_btn.setMaximumWidth(30)
64
+ remove_btn.setStyleSheet("QPushButton { color: red; font-weight: bold; }")
65
+ remove_btn.clicked.connect(lambda: self.remove_requested.emit(self))
66
+ layout.addWidget(remove_btn)
67
+
68
+ layout.addStretch()
69
+
70
+ def set_operators(self, operators: List[Dict[str, Any]]):
71
+ """Set available operators from connection provider."""
72
+ self.operators = operators
73
+ self.operator_combo.clear()
74
+ for op in operators:
75
+ name = op["name"]
76
+ server_side = op.get("server_side", True)
77
+ if not server_side:
78
+ name = f"{name} (client-side)"
79
+ self.operator_combo.addItem(name)
80
+
81
+ def set_available_fields(self, fields: List[str]):
82
+ """Set the available field names in the dropdown."""
83
+ current_text = self.field_input.currentText()
84
+ self.field_input.clear()
85
+ self.field_input.addItems(fields)
86
+ # Restore the current text if it was set
87
+ if current_text:
88
+ self.field_input.setEditText(current_text)
89
+
90
+ def get_filter_dict(self) -> Optional[Dict[str, Any]]:
91
+ """Get the filter as a dictionary."""
92
+ field = self.field_input.currentText().strip()
93
+ operator_display = self.operator_combo.currentText()
94
+ value_text = self.value_input.text().strip()
95
+
96
+ if not field or not value_text:
97
+ return None
98
+
99
+ # Strip (client-side) suffix if present
100
+ operator = operator_display.replace(" (client-side)", "")
101
+
102
+ # Check if this is a client-side operator
103
+ is_client_side = "(client-side)" in operator_display
104
+
105
+ # Parse value (try to convert to appropriate type)
106
+ value = self._parse_value(value_text)
107
+
108
+ # Handle client-side operators
109
+ if is_client_side:
110
+ if operator == "contains":
111
+ return {"__client_side__": True, "field": field, "op": "contains", "value": value}
112
+ elif operator == "not contains":
113
+ return {"__client_side__": True, "field": field, "op": "not_contains", "value": value}
114
+
115
+ # For server-side text operators, use special syntax
116
+ if operator == "contains":
117
+ return {field: {"$contains": value}}
118
+ elif operator == "not contains":
119
+ return {field: {"$not_contains": value}}
120
+
121
+ # Map operator to database syntax (server-side)
122
+ if operator == "=":
123
+ return {field: {"$eq": value}}
124
+ elif operator == "!=":
125
+ return {field: {"$ne": value}}
126
+ elif operator == ">":
127
+ return {field: {"$gt": value}}
128
+ elif operator == ">=":
129
+ return {field: {"$gte": value}}
130
+ elif operator == "<":
131
+ return {field: {"$lt": value}}
132
+ elif operator == "<=":
133
+ return {field: {"$lte": value}}
134
+ elif operator == "in":
135
+ # Value should be comma-separated list
136
+ values = [self._parse_value(v.strip()) for v in value_text.split(",")]
137
+ return {field: {"$in": values}}
138
+ elif operator == "not in":
139
+ values = [self._parse_value(v.strip()) for v in value_text.split(",")]
140
+ return {field: {"$nin": values}}
141
+
142
+ return None
143
+
144
+ def _parse_value(self, value_text: str) -> Any:
145
+ """Parse value text to appropriate type."""
146
+ # Try to parse as number
147
+ try:
148
+ if "." in value_text:
149
+ return float(value_text)
150
+ return int(value_text)
151
+ except ValueError:
152
+ pass
153
+
154
+ # Try to parse as boolean
155
+ if value_text.lower() == "true":
156
+ return True
157
+ elif value_text.lower() == "false":
158
+ return False
159
+
160
+ # Return as string
161
+ return value_text
162
+
163
+
164
+ class FilterBuilder(QWidget):
165
+ """Advanced metadata filter builder widget."""
166
+
167
+ filter_changed = Signal() # Signal when filter changes
168
+ apply_filters = Signal() # Signal when user wants to apply filters
169
+
170
+ def __init__(self, parent=None):
171
+ super().__init__(parent)
172
+ self.rules: List[FilterRule] = []
173
+ self.available_fields: List[str] = [] # Store available field names
174
+ self.operators: List[Dict[str, Any]] = [] # Store operators from connection
175
+ self._setup_ui()
176
+
177
+ def _setup_ui(self):
178
+ """Setup the filter builder UI."""
179
+ layout = QVBoxLayout(self)
180
+ layout.setContentsMargins(0, 0, 0, 0)
181
+
182
+ # Header with controls
183
+ header_layout = QHBoxLayout()
184
+
185
+ # Logic operator (AND/OR)
186
+ header_layout.addWidget(QLabel("Combine rules with:"))
187
+ self.logic_combo = QComboBox()
188
+ self.logic_combo.addItems(["AND", "OR"])
189
+ self.logic_combo.currentTextChanged.connect(self.filter_changed.emit)
190
+ header_layout.addWidget(self.logic_combo)
191
+
192
+ header_layout.addStretch()
193
+
194
+ # Add rule button
195
+ add_btn = QPushButton("+ Add Filter Rule")
196
+ add_btn.clicked.connect(self._add_rule)
197
+ header_layout.addWidget(add_btn)
198
+
199
+ # Clear all button
200
+ clear_btn = QPushButton("Clear All")
201
+ clear_btn.clicked.connect(self._clear_all)
202
+ header_layout.addWidget(clear_btn)
203
+
204
+ layout.addLayout(header_layout)
205
+
206
+ # Separator
207
+ line = QFrame()
208
+ line.setFrameShape(QFrame.HLine)
209
+ line.setFrameShadow(QFrame.Sunken)
210
+ layout.addWidget(line)
211
+
212
+ # Scroll area for rules
213
+ scroll_area = QScrollArea()
214
+ scroll_area.setWidgetResizable(True)
215
+ scroll_area.setFrameShape(QFrame.NoFrame)
216
+
217
+ self.rules_container = QWidget()
218
+ self.rules_layout = QVBoxLayout(self.rules_container)
219
+ self.rules_layout.setAlignment(Qt.AlignTop)
220
+ self.rules_layout.setContentsMargins(0, 5, 0, 5)
221
+
222
+ # Placeholder label
223
+ self.placeholder_label = QLabel("No filters applied. Click '+ Add Filter Rule' to start.")
224
+ self.placeholder_label.setStyleSheet("color: gray; font-style: italic; padding: 20px;")
225
+ self.placeholder_label.setAlignment(Qt.AlignCenter)
226
+ self.rules_layout.addWidget(self.placeholder_label)
227
+
228
+ scroll_area.setWidget(self.rules_container)
229
+ layout.addWidget(scroll_area)
230
+
231
+ def _add_rule(self):
232
+ """Add a new filter rule."""
233
+ rule = FilterRule()
234
+ rule.remove_requested.connect(self._remove_rule)
235
+ rule.apply_requested.connect(self.apply_filters.emit)
236
+ rule.field_input.editTextChanged.connect(lambda: self.filter_changed.emit())
237
+ rule.operator_combo.currentTextChanged.connect(lambda: self.filter_changed.emit())
238
+ rule.value_input.textChanged.connect(lambda: self.filter_changed.emit())
239
+
240
+ # Apply available fields if we have them
241
+ if self.available_fields:
242
+ rule.set_available_fields(self.available_fields)
243
+
244
+ # Apply operators if we have them
245
+ if self.operators:
246
+ rule.set_operators(self.operators)
247
+
248
+ self.rules.append(rule)
249
+
250
+ # Hide placeholder if this is the first rule
251
+ if len(self.rules) == 1:
252
+ self.placeholder_label.hide()
253
+
254
+ self.rules_layout.addWidget(rule)
255
+ self.filter_changed.emit()
256
+
257
+ def _remove_rule(self, rule: FilterRule):
258
+ """Remove a filter rule."""
259
+ if rule in self.rules:
260
+ self.rules.remove(rule)
261
+ rule.deleteLater()
262
+
263
+ # Show placeholder if no more rules
264
+ if len(self.rules) == 0:
265
+ self.placeholder_label.show()
266
+
267
+ self.filter_changed.emit()
268
+
269
+ def _clear_all(self):
270
+ """Clear all filter rules."""
271
+ for rule in self.rules[:]:
272
+ self._remove_rule(rule)
273
+
274
+ def get_filter(self) -> Optional[Dict[str, Any]]:
275
+ """
276
+ Get the complete filter as a dictionary suitable for vector DB queries.
277
+
278
+ Returns:
279
+ Filter dictionary or None if no rules
280
+ """
281
+ if not self.rules:
282
+ return None
283
+
284
+ # Get all rule filters
285
+ rule_filters = []
286
+ for rule in self.rules:
287
+ rule_filter = rule.get_filter_dict()
288
+ if rule_filter:
289
+ rule_filters.append(rule_filter)
290
+
291
+ if not rule_filters:
292
+ return None
293
+
294
+ # Combine with logic operator
295
+ logic = self.logic_combo.currentText().lower()
296
+
297
+ if len(rule_filters) == 1:
298
+ return rule_filters[0]
299
+
300
+ # Combine multiple filters
301
+ return {f"${logic}": rule_filters}
302
+
303
+ def get_filters_split(self) -> tuple[Optional[Dict[str, Any]], list[Dict[str, Any]]]:
304
+ """
305
+ Get filters split into server-side and client-side filters.
306
+
307
+ Returns:
308
+ Tuple of (server_side_filter, client_side_filters_list)
309
+ """
310
+ if not self.rules:
311
+ return None, []
312
+
313
+ server_side_filters = []
314
+ client_side_filters = []
315
+
316
+ for rule in self.rules:
317
+ rule_filter = rule.get_filter_dict()
318
+ if rule_filter:
319
+ if rule_filter.get("__client_side__"):
320
+ client_side_filters.append(rule_filter)
321
+ else:
322
+ server_side_filters.append(rule_filter)
323
+
324
+ # Build server-side filter
325
+ server_filter = None
326
+ if server_side_filters:
327
+ logic = self.logic_combo.currentText().lower()
328
+ if len(server_side_filters) == 1:
329
+ server_filter = server_side_filters[0]
330
+ else:
331
+ server_filter = {f"${logic}": server_side_filters}
332
+
333
+ return server_filter, client_side_filters
334
+
335
+ def has_filters(self) -> bool:
336
+ """Check if any filters are defined."""
337
+ return len(self.rules) > 0
338
+
339
+ def set_available_fields(self, fields: List[str]):
340
+ """Set available field names for all filter rules."""
341
+ self.available_fields = fields # Store for future rules
342
+ for rule in self.rules:
343
+ rule.set_available_fields(fields)
344
+
345
+ def set_operators(self, operators: List[Dict[str, Any]]):
346
+ """Set available operators for all filter rules."""
347
+ self.operators = operators # Store for future rules
348
+ for rule in self.rules:
349
+ rule.set_operators(operators)
350
+
351
+ def get_filter_summary(self) -> str:
352
+ """Get a human-readable summary of the current filters."""
353
+ if not self.rules:
354
+ return "No filters"
355
+
356
+ logic = self.logic_combo.currentText()
357
+ rule_summaries = []
358
+
359
+ for rule in self.rules:
360
+ field = rule.field_input.currentText().strip()
361
+ operator = rule.operator_combo.currentText()
362
+ value = rule.value_input.text().strip()
363
+
364
+ if field and value:
365
+ rule_summaries.append(f"{field} {operator} {value}")
366
+
367
+ if not rule_summaries:
368
+ return "No valid filters"
369
+
370
+ return f" {logic} ".join(rule_summaries)
@@ -0,0 +1,118 @@
1
+ """Dialog for adding/editing items in a collection."""
2
+
3
+ from typing import Optional, Dict, Any
4
+ from PySide6.QtWidgets import (
5
+ QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
6
+ QLineEdit, QTextEdit, QPushButton, QLabel, QMessageBox
7
+ )
8
+ from PySide6.QtCore import Qt
9
+
10
+
11
+ class ItemDialog(QDialog):
12
+ """Dialog for adding or editing a vector item."""
13
+
14
+ def __init__(self, parent=None, item_data: Optional[Dict[str, Any]] = None):
15
+ super().__init__(parent)
16
+ self.item_data = item_data
17
+ self.is_edit_mode = item_data is not None
18
+
19
+ self.setWindowTitle("Edit Item" if self.is_edit_mode else "Add Item")
20
+ self.setMinimumWidth(500)
21
+ self.setMinimumHeight(400)
22
+
23
+ self._setup_ui()
24
+
25
+ if self.is_edit_mode:
26
+ self._populate_fields()
27
+
28
+ def _setup_ui(self):
29
+ """Setup dialog UI."""
30
+ layout = QVBoxLayout(self)
31
+
32
+ # Form layout
33
+ form_layout = QFormLayout()
34
+
35
+ # ID field
36
+ self.id_input = QLineEdit()
37
+ if self.is_edit_mode:
38
+ self.id_input.setReadOnly(True)
39
+ form_layout.addRow("ID:", self.id_input)
40
+
41
+ # Document field
42
+ form_layout.addRow("Document:", QLabel(""))
43
+ self.document_input = QTextEdit()
44
+ self.document_input.setMaximumHeight(150)
45
+ form_layout.addRow(self.document_input)
46
+
47
+ # Metadata field
48
+ form_layout.addRow("Metadata (JSON):", QLabel(""))
49
+ self.metadata_input = QTextEdit()
50
+ self.metadata_input.setMaximumHeight(150)
51
+ self.metadata_input.setPlaceholderText('{"key": "value", "category": "example"}')
52
+ form_layout.addRow(self.metadata_input)
53
+
54
+ layout.addLayout(form_layout)
55
+
56
+ # Note about embeddings
57
+ note_label = QLabel(
58
+ "Note: Embeddings will be automatically generated from the document text."
59
+ )
60
+ note_label.setStyleSheet("color: gray; font-style: italic;")
61
+ note_label.setWordWrap(True)
62
+ layout.addWidget(note_label)
63
+
64
+ # Buttons
65
+ button_layout = QHBoxLayout()
66
+
67
+ save_button = QPushButton("Save" if self.is_edit_mode else "Add")
68
+ save_button.clicked.connect(self.accept)
69
+ save_button.setDefault(True)
70
+
71
+ cancel_button = QPushButton("Cancel")
72
+ cancel_button.clicked.connect(self.reject)
73
+
74
+ button_layout.addStretch()
75
+ button_layout.addWidget(save_button)
76
+ button_layout.addWidget(cancel_button)
77
+
78
+ layout.addLayout(button_layout)
79
+
80
+ def _populate_fields(self):
81
+ """Populate fields with existing item data."""
82
+ if not self.item_data:
83
+ return
84
+
85
+ self.id_input.setText(str(self.item_data.get("id", "")))
86
+ self.document_input.setPlainText(str(self.item_data.get("document", "")))
87
+
88
+ metadata = self.item_data.get("metadata", {})
89
+ if metadata:
90
+ import json
91
+ self.metadata_input.setPlainText(json.dumps(metadata, indent=2))
92
+
93
+ def get_item_data(self) -> Dict[str, Any]:
94
+ """Get item data from dialog fields."""
95
+ import json
96
+
97
+ item_id = self.id_input.text().strip()
98
+ document = self.document_input.toPlainText().strip()
99
+
100
+ # Parse metadata
101
+ metadata = {}
102
+ metadata_text = self.metadata_input.toPlainText().strip()
103
+ if metadata_text:
104
+ try:
105
+ metadata = json.loads(metadata_text)
106
+ except json.JSONDecodeError:
107
+ QMessageBox.warning(
108
+ self,
109
+ "Invalid Metadata",
110
+ "Metadata must be valid JSON format."
111
+ )
112
+ return None
113
+
114
+ return {
115
+ "id": item_id,
116
+ "document": document,
117
+ "metadata": metadata
118
+ }
@@ -0,0 +1,30 @@
1
+ from PySide6.QtWidgets import QProgressDialog, QApplication
2
+ from PySide6.QtCore import Qt
3
+
4
+ class LoadingDialog(QProgressDialog):
5
+ def __init__(self, message="Loading...", parent=None):
6
+ super().__init__(message, None, 0, 0, parent)
7
+ self.setWindowTitle("Please Wait")
8
+ self.setWindowModality(Qt.ApplicationModal)
9
+ self.setCancelButton(None)
10
+ self.setMinimumDuration(0)
11
+ self.setAutoClose(False)
12
+ self.setAutoReset(False)
13
+ self.setValue(0)
14
+ self.setMinimumWidth(300)
15
+ self.reset() # Hide dialog by default until show_loading() is called
16
+
17
+ def show_loading(self, message=None):
18
+ if message:
19
+ self.setLabelText(message)
20
+ self.setValue(0)
21
+ self.show()
22
+ # Force the dialog to render by processing events multiple times
23
+ QApplication.processEvents()
24
+ self.repaint()
25
+ QApplication.processEvents()
26
+
27
+ def hide_loading(self):
28
+ self.reset()
29
+ self.hide()
30
+ self.close()