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.
- vector_inspector/__init__.py +3 -0
- vector_inspector/__main__.py +4 -0
- vector_inspector/core/__init__.py +1 -0
- vector_inspector/core/connections/__init__.py +7 -0
- vector_inspector/core/connections/base_connection.py +233 -0
- vector_inspector/core/connections/chroma_connection.py +384 -0
- vector_inspector/core/connections/qdrant_connection.py +723 -0
- vector_inspector/core/connections/template_connection.py +346 -0
- vector_inspector/main.py +21 -0
- vector_inspector/services/__init__.py +1 -0
- vector_inspector/services/backup_restore_service.py +286 -0
- vector_inspector/services/filter_service.py +72 -0
- vector_inspector/services/import_export_service.py +287 -0
- vector_inspector/services/settings_service.py +60 -0
- vector_inspector/services/visualization_service.py +116 -0
- vector_inspector/ui/__init__.py +1 -0
- vector_inspector/ui/components/__init__.py +1 -0
- vector_inspector/ui/components/backup_restore_dialog.py +350 -0
- vector_inspector/ui/components/filter_builder.py +370 -0
- vector_inspector/ui/components/item_dialog.py +118 -0
- vector_inspector/ui/components/loading_dialog.py +30 -0
- vector_inspector/ui/main_window.py +288 -0
- vector_inspector/ui/views/__init__.py +1 -0
- vector_inspector/ui/views/collection_browser.py +112 -0
- vector_inspector/ui/views/connection_view.py +423 -0
- vector_inspector/ui/views/metadata_view.py +555 -0
- vector_inspector/ui/views/search_view.py +268 -0
- vector_inspector/ui/views/visualization_view.py +245 -0
- vector_inspector-0.2.0.dist-info/METADATA +382 -0
- vector_inspector-0.2.0.dist-info/RECORD +32 -0
- vector_inspector-0.2.0.dist-info/WHEEL +4 -0
- vector_inspector-0.2.0.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
"""Metadata browsing and data view."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Dict, Any, List
|
|
4
|
+
from PySide6.QtWidgets import (
|
|
5
|
+
QWidget, QVBoxLayout, QHBoxLayout, QTableWidget,
|
|
6
|
+
QTableWidgetItem, QPushButton, QLabel, QSpinBox,
|
|
7
|
+
QLineEdit, QComboBox, QGroupBox, QHeaderView, QMessageBox, QDialog,
|
|
8
|
+
QFileDialog, QMenu
|
|
9
|
+
)
|
|
10
|
+
from PySide6.QtCore import Qt, QTimer
|
|
11
|
+
|
|
12
|
+
from vector_inspector.core.connections.base_connection import VectorDBConnection
|
|
13
|
+
from vector_inspector.ui.components.item_dialog import ItemDialog
|
|
14
|
+
from vector_inspector.ui.components.loading_dialog import LoadingDialog
|
|
15
|
+
from vector_inspector.ui.components.filter_builder import FilterBuilder
|
|
16
|
+
from vector_inspector.services.import_export_service import ImportExportService
|
|
17
|
+
from vector_inspector.services.filter_service import apply_client_side_filters
|
|
18
|
+
from PySide6.QtWidgets import QApplication
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MetadataView(QWidget):
|
|
22
|
+
"""View for browsing collection data and metadata."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, connection: VectorDBConnection, parent=None):
|
|
25
|
+
super().__init__(parent)
|
|
26
|
+
self.connection = connection
|
|
27
|
+
self.current_collection: str = ""
|
|
28
|
+
self.current_data: Optional[Dict[str, Any]] = None
|
|
29
|
+
self.page_size = 50
|
|
30
|
+
self.current_page = 0
|
|
31
|
+
self.loading_dialog = LoadingDialog("Loading data...", self)
|
|
32
|
+
|
|
33
|
+
# Debounce timer for filter changes
|
|
34
|
+
self.filter_reload_timer = QTimer()
|
|
35
|
+
self.filter_reload_timer.setSingleShot(True)
|
|
36
|
+
self.filter_reload_timer.timeout.connect(self._reload_with_filters)
|
|
37
|
+
|
|
38
|
+
self._setup_ui()
|
|
39
|
+
|
|
40
|
+
def _setup_ui(self):
|
|
41
|
+
"""Setup widget UI."""
|
|
42
|
+
layout = QVBoxLayout(self)
|
|
43
|
+
|
|
44
|
+
# Controls
|
|
45
|
+
controls_layout = QHBoxLayout()
|
|
46
|
+
|
|
47
|
+
# Pagination controls
|
|
48
|
+
controls_layout.addWidget(QLabel("Page:"))
|
|
49
|
+
|
|
50
|
+
self.prev_button = QPushButton("◀ Previous")
|
|
51
|
+
self.prev_button.clicked.connect(self._previous_page)
|
|
52
|
+
self.prev_button.setEnabled(False)
|
|
53
|
+
controls_layout.addWidget(self.prev_button)
|
|
54
|
+
|
|
55
|
+
self.page_label = QLabel("0 / 0")
|
|
56
|
+
controls_layout.addWidget(self.page_label)
|
|
57
|
+
|
|
58
|
+
self.next_button = QPushButton("Next ▶")
|
|
59
|
+
self.next_button.clicked.connect(self._next_page)
|
|
60
|
+
self.next_button.setEnabled(False)
|
|
61
|
+
controls_layout.addWidget(self.next_button)
|
|
62
|
+
|
|
63
|
+
controls_layout.addWidget(QLabel(" Items per page:"))
|
|
64
|
+
|
|
65
|
+
self.page_size_spin = QSpinBox()
|
|
66
|
+
self.page_size_spin.setMinimum(10)
|
|
67
|
+
self.page_size_spin.setMaximum(500)
|
|
68
|
+
self.page_size_spin.setValue(50)
|
|
69
|
+
self.page_size_spin.setSingleStep(10)
|
|
70
|
+
self.page_size_spin.valueChanged.connect(self._on_page_size_changed)
|
|
71
|
+
controls_layout.addWidget(self.page_size_spin)
|
|
72
|
+
|
|
73
|
+
controls_layout.addStretch()
|
|
74
|
+
|
|
75
|
+
# Refresh button
|
|
76
|
+
self.refresh_button = QPushButton("Refresh")
|
|
77
|
+
self.refresh_button.clicked.connect(self._load_data)
|
|
78
|
+
controls_layout.addWidget(self.refresh_button)
|
|
79
|
+
|
|
80
|
+
# Add/Delete buttons
|
|
81
|
+
self.add_button = QPushButton("Add Item")
|
|
82
|
+
self.add_button.clicked.connect(self._add_item)
|
|
83
|
+
controls_layout.addWidget(self.add_button)
|
|
84
|
+
|
|
85
|
+
self.delete_button = QPushButton("Delete Selected")
|
|
86
|
+
self.delete_button.clicked.connect(self._delete_selected)
|
|
87
|
+
controls_layout.addWidget(self.delete_button)
|
|
88
|
+
|
|
89
|
+
# Export button with menu
|
|
90
|
+
self.export_button = QPushButton("Export...")
|
|
91
|
+
self.export_button.setStyleSheet("QPushButton::menu-indicator { width: 0px; }")
|
|
92
|
+
export_menu = QMenu(self)
|
|
93
|
+
export_menu.addAction("Export to JSON", lambda: self._export_data("json"))
|
|
94
|
+
export_menu.addAction("Export to CSV", lambda: self._export_data("csv"))
|
|
95
|
+
export_menu.addAction("Export to Parquet", lambda: self._export_data("parquet"))
|
|
96
|
+
self.export_button.setMenu(export_menu)
|
|
97
|
+
controls_layout.addWidget(self.export_button)
|
|
98
|
+
|
|
99
|
+
# Import button with menu
|
|
100
|
+
self.import_button = QPushButton("Import...")
|
|
101
|
+
self.import_button.setStyleSheet("QPushButton::menu-indicator { width: 0px; }")
|
|
102
|
+
import_menu = QMenu(self)
|
|
103
|
+
import_menu.addAction("Import from JSON", lambda: self._import_data("json"))
|
|
104
|
+
import_menu.addAction("Import from CSV", lambda: self._import_data("csv"))
|
|
105
|
+
import_menu.addAction("Import from Parquet", lambda: self._import_data("parquet"))
|
|
106
|
+
self.import_button.setMenu(import_menu)
|
|
107
|
+
controls_layout.addWidget(self.import_button)
|
|
108
|
+
|
|
109
|
+
layout.addLayout(controls_layout)
|
|
110
|
+
|
|
111
|
+
# Filter section
|
|
112
|
+
filter_group = QGroupBox("Metadata Filters")
|
|
113
|
+
filter_group.setCheckable(True)
|
|
114
|
+
filter_group.setChecked(False)
|
|
115
|
+
filter_group_layout = QVBoxLayout()
|
|
116
|
+
|
|
117
|
+
self.filter_builder = FilterBuilder()
|
|
118
|
+
# Remove auto-reload on filter changes - only reload when user clicks Refresh
|
|
119
|
+
# self.filter_builder.filter_changed.connect(self._on_filter_changed)
|
|
120
|
+
# But DO reload when user presses Enter or clicks away from value input
|
|
121
|
+
self.filter_builder.apply_filters.connect(self._apply_filters)
|
|
122
|
+
filter_group_layout.addWidget(self.filter_builder)
|
|
123
|
+
|
|
124
|
+
filter_group.setLayout(filter_group_layout)
|
|
125
|
+
layout.addWidget(filter_group)
|
|
126
|
+
self.filter_group = filter_group
|
|
127
|
+
|
|
128
|
+
# Data table
|
|
129
|
+
self.table = QTableWidget()
|
|
130
|
+
self.table.setSelectionBehavior(QTableWidget.SelectRows)
|
|
131
|
+
self.table.setAlternatingRowColors(True)
|
|
132
|
+
self.table.horizontalHeader().setStretchLastSection(True)
|
|
133
|
+
self.table.doubleClicked.connect(self._on_row_double_clicked)
|
|
134
|
+
layout.addWidget(self.table)
|
|
135
|
+
|
|
136
|
+
# Status bar
|
|
137
|
+
self.status_label = QLabel("No collection selected")
|
|
138
|
+
self.status_label.setStyleSheet("color: gray;")
|
|
139
|
+
layout.addWidget(self.status_label)
|
|
140
|
+
|
|
141
|
+
def set_collection(self, collection_name: str):
|
|
142
|
+
"""Set the current collection to display."""
|
|
143
|
+
self.current_collection = collection_name
|
|
144
|
+
self.current_page = 0
|
|
145
|
+
|
|
146
|
+
# Show loading dialog at the start
|
|
147
|
+
self.loading_dialog.show_loading("Loading collection data...")
|
|
148
|
+
QApplication.processEvents()
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
# Update filter builder with supported operators
|
|
152
|
+
operators = self.connection.get_supported_filter_operators()
|
|
153
|
+
self.filter_builder.set_operators(operators)
|
|
154
|
+
|
|
155
|
+
self._load_data_internal()
|
|
156
|
+
|
|
157
|
+
# Ensure UI is fully updated before hiding loading dialog
|
|
158
|
+
QApplication.processEvents()
|
|
159
|
+
finally:
|
|
160
|
+
self.loading_dialog.hide_loading()
|
|
161
|
+
|
|
162
|
+
def _load_data(self):
|
|
163
|
+
"""Load data from current collection (with loading dialog)."""
|
|
164
|
+
if not self.current_collection:
|
|
165
|
+
self.status_label.setText("No collection selected")
|
|
166
|
+
self.table.setRowCount(0)
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
self.loading_dialog.show_loading("Loading data from collection...")
|
|
170
|
+
QApplication.processEvents()
|
|
171
|
+
try:
|
|
172
|
+
self._load_data_internal()
|
|
173
|
+
finally:
|
|
174
|
+
self.loading_dialog.hide_loading()
|
|
175
|
+
|
|
176
|
+
def _load_data_internal(self):
|
|
177
|
+
"""Internal method to load data without managing loading dialog."""
|
|
178
|
+
if not self.current_collection:
|
|
179
|
+
self.status_label.setText("No collection selected")
|
|
180
|
+
self.table.setRowCount(0)
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
offset = self.current_page * self.page_size
|
|
184
|
+
|
|
185
|
+
# Get filters split into server-side and client-side
|
|
186
|
+
server_filter = None
|
|
187
|
+
client_filters = []
|
|
188
|
+
if self.filter_group.isChecked() and self.filter_builder.has_filters():
|
|
189
|
+
server_filter, client_filters = self.filter_builder.get_filters_split()
|
|
190
|
+
|
|
191
|
+
data = self.connection.get_all_items(
|
|
192
|
+
self.current_collection,
|
|
193
|
+
limit=self.page_size,
|
|
194
|
+
offset=offset,
|
|
195
|
+
where=server_filter
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Apply client-side filters if any
|
|
199
|
+
if client_filters and data:
|
|
200
|
+
data = apply_client_side_filters(data, client_filters)
|
|
201
|
+
|
|
202
|
+
if not data:
|
|
203
|
+
self.status_label.setText("Failed to load data")
|
|
204
|
+
self.table.setRowCount(0)
|
|
205
|
+
return
|
|
206
|
+
self.current_data = data
|
|
207
|
+
self._populate_table(data)
|
|
208
|
+
self._update_pagination_controls()
|
|
209
|
+
|
|
210
|
+
# Update filter builder with available metadata fields
|
|
211
|
+
self._update_filter_fields(data)
|
|
212
|
+
|
|
213
|
+
def _update_filter_fields(self, data: Dict[str, Any]):
|
|
214
|
+
"""Update filter builder with available metadata field names."""
|
|
215
|
+
field_names = []
|
|
216
|
+
|
|
217
|
+
# Add 'document' field if documents exist
|
|
218
|
+
documents = data.get("documents", [])
|
|
219
|
+
if documents and any(doc for doc in documents if doc):
|
|
220
|
+
field_names.append("document")
|
|
221
|
+
|
|
222
|
+
# Add metadata fields
|
|
223
|
+
metadatas = data.get("metadatas", [])
|
|
224
|
+
if metadatas and len(metadatas) > 0 and metadatas[0]:
|
|
225
|
+
# Get all unique metadata keys from the first item
|
|
226
|
+
metadata_keys = sorted(metadatas[0].keys())
|
|
227
|
+
field_names.extend(metadata_keys)
|
|
228
|
+
|
|
229
|
+
if field_names:
|
|
230
|
+
self.filter_builder.set_available_fields(field_names)
|
|
231
|
+
|
|
232
|
+
def _populate_table(self, data: Dict[str, Any]):
|
|
233
|
+
"""Populate table with data."""
|
|
234
|
+
ids = data.get("ids", [])
|
|
235
|
+
documents = data.get("documents", [])
|
|
236
|
+
metadatas = data.get("metadatas", [])
|
|
237
|
+
|
|
238
|
+
if not ids:
|
|
239
|
+
self.table.setRowCount(0)
|
|
240
|
+
self.status_label.setText("No data in collection")
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
# Determine columns
|
|
244
|
+
columns = ["ID", "Document"]
|
|
245
|
+
if metadatas and metadatas[0]:
|
|
246
|
+
metadata_keys = list(metadatas[0].keys())
|
|
247
|
+
columns.extend(metadata_keys)
|
|
248
|
+
|
|
249
|
+
self.table.setColumnCount(len(columns))
|
|
250
|
+
self.table.setHorizontalHeaderLabels(columns)
|
|
251
|
+
self.table.setRowCount(len(ids))
|
|
252
|
+
|
|
253
|
+
# Populate rows
|
|
254
|
+
for row, (id_val, doc, meta) in enumerate(zip(ids, documents, metadatas)):
|
|
255
|
+
# ID column
|
|
256
|
+
self.table.setItem(row, 0, QTableWidgetItem(str(id_val)))
|
|
257
|
+
|
|
258
|
+
# Document column
|
|
259
|
+
doc_text = str(doc) if doc else ""
|
|
260
|
+
if len(doc_text) > 100:
|
|
261
|
+
doc_text = doc_text[:100] + "..."
|
|
262
|
+
self.table.setItem(row, 1, QTableWidgetItem(doc_text))
|
|
263
|
+
|
|
264
|
+
# Metadata columns
|
|
265
|
+
if meta:
|
|
266
|
+
for col_idx, key in enumerate(metadata_keys, start=2):
|
|
267
|
+
value = meta.get(key, "")
|
|
268
|
+
self.table.setItem(row, col_idx, QTableWidgetItem(str(value)))
|
|
269
|
+
|
|
270
|
+
self.table.resizeColumnsToContents()
|
|
271
|
+
self.status_label.setText(f"Showing {len(ids)} items")
|
|
272
|
+
|
|
273
|
+
def _update_pagination_controls(self):
|
|
274
|
+
"""Update pagination button states."""
|
|
275
|
+
if not self.current_data:
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
item_count = len(self.current_data.get("ids", []))
|
|
279
|
+
has_more = item_count == self.page_size
|
|
280
|
+
|
|
281
|
+
self.prev_button.setEnabled(self.current_page > 0)
|
|
282
|
+
self.next_button.setEnabled(has_more)
|
|
283
|
+
|
|
284
|
+
# Update page label (approximate since ChromaDB doesn't give total count easily)
|
|
285
|
+
self.page_label.setText(f"{self.current_page + 1}")
|
|
286
|
+
|
|
287
|
+
def _previous_page(self):
|
|
288
|
+
"""Go to previous page."""
|
|
289
|
+
if self.current_page > 0:
|
|
290
|
+
self.current_page -= 1
|
|
291
|
+
self._load_data()
|
|
292
|
+
|
|
293
|
+
def _next_page(self):
|
|
294
|
+
"""Go to next page."""
|
|
295
|
+
self.current_page += 1
|
|
296
|
+
self._load_data()
|
|
297
|
+
|
|
298
|
+
def _on_page_size_changed(self, value: int):
|
|
299
|
+
"""Handle page size change."""
|
|
300
|
+
self.page_size = value
|
|
301
|
+
self.current_page = 0
|
|
302
|
+
self._load_data()
|
|
303
|
+
|
|
304
|
+
def _add_item(self):
|
|
305
|
+
"""Add a new item to the collection."""
|
|
306
|
+
if not self.current_collection:
|
|
307
|
+
QMessageBox.warning(self, "No Collection", "Please select a collection first.")
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
dialog = ItemDialog(self)
|
|
311
|
+
|
|
312
|
+
if dialog.exec() == QDialog.Accepted:
|
|
313
|
+
item_data = dialog.get_item_data()
|
|
314
|
+
if not item_data:
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
# Add item to collection
|
|
318
|
+
success = self.connection.add_items(
|
|
319
|
+
self.current_collection,
|
|
320
|
+
documents=[item_data["document"]],
|
|
321
|
+
metadatas=[item_data["metadata"]] if item_data["metadata"] else None,
|
|
322
|
+
ids=[item_data["id"]] if item_data["id"] else None
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
if success:
|
|
326
|
+
QMessageBox.information(self, "Success", "Item added successfully.")
|
|
327
|
+
self._load_data()
|
|
328
|
+
else:
|
|
329
|
+
QMessageBox.warning(self, "Error", "Failed to add item.")
|
|
330
|
+
|
|
331
|
+
def _delete_selected(self):
|
|
332
|
+
"""Delete selected items."""
|
|
333
|
+
if not self.current_collection:
|
|
334
|
+
QMessageBox.warning(self, "No Collection", "Please select a collection first.")
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
selected_rows = self.table.selectionModel().selectedRows()
|
|
338
|
+
if not selected_rows:
|
|
339
|
+
QMessageBox.warning(self, "No Selection", "Please select items to delete.")
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
# Get IDs of selected items
|
|
343
|
+
ids_to_delete = []
|
|
344
|
+
for row in selected_rows:
|
|
345
|
+
id_item = self.table.item(row.row(), 0)
|
|
346
|
+
if id_item:
|
|
347
|
+
ids_to_delete.append(id_item.text())
|
|
348
|
+
|
|
349
|
+
# Confirm deletion
|
|
350
|
+
reply = QMessageBox.question(
|
|
351
|
+
self,
|
|
352
|
+
"Confirm Deletion",
|
|
353
|
+
f"Delete {len(ids_to_delete)} item(s)?",
|
|
354
|
+
QMessageBox.Yes | QMessageBox.No
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
if reply == QMessageBox.Yes:
|
|
358
|
+
success = self.connection.delete_items(self.current_collection, ids=ids_to_delete)
|
|
359
|
+
if success:
|
|
360
|
+
QMessageBox.information(self, "Success", "Items deleted successfully.")
|
|
361
|
+
self._load_data()
|
|
362
|
+
else:
|
|
363
|
+
QMessageBox.warning(self, "Error", "Failed to delete items.")
|
|
364
|
+
|
|
365
|
+
def _on_filter_changed(self):
|
|
366
|
+
"""Handle filter changes - debounce and reload data."""
|
|
367
|
+
if self.filter_group.isChecked():
|
|
368
|
+
# Restart the timer - will only fire 500ms after last change
|
|
369
|
+
self.filter_reload_timer.stop()
|
|
370
|
+
self.filter_reload_timer.start(500) # 500ms debounce
|
|
371
|
+
|
|
372
|
+
def _reload_with_filters(self):
|
|
373
|
+
"""Reload data with current filters (called after debounce)."""
|
|
374
|
+
self.current_page = 0
|
|
375
|
+
self._load_data()
|
|
376
|
+
|
|
377
|
+
def _apply_filters(self):
|
|
378
|
+
"""Apply filters when user presses Enter or clicks away."""
|
|
379
|
+
if self.filter_group.isChecked() and self.current_collection:
|
|
380
|
+
self.current_page = 0
|
|
381
|
+
self._load_data()
|
|
382
|
+
|
|
383
|
+
def _on_row_double_clicked(self, index):
|
|
384
|
+
"""Handle double-click on a row to edit item."""
|
|
385
|
+
if not self.current_collection or not self.current_data:
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
row = index.row()
|
|
389
|
+
if row < 0 or row >= self.table.rowCount():
|
|
390
|
+
return
|
|
391
|
+
|
|
392
|
+
# Get item data for this row
|
|
393
|
+
ids = self.current_data.get("ids", [])
|
|
394
|
+
documents = self.current_data.get("documents", [])
|
|
395
|
+
metadatas = self.current_data.get("metadatas", [])
|
|
396
|
+
|
|
397
|
+
if row >= len(ids):
|
|
398
|
+
return
|
|
399
|
+
|
|
400
|
+
item_data = {
|
|
401
|
+
"id": ids[row],
|
|
402
|
+
"document": documents[row] if row < len(documents) else "",
|
|
403
|
+
"metadata": metadatas[row] if row < len(metadatas) else {}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
# Open edit dialog
|
|
407
|
+
dialog = ItemDialog(self, item_data=item_data)
|
|
408
|
+
|
|
409
|
+
if dialog.exec() == QDialog.Accepted:
|
|
410
|
+
updated_data = dialog.get_item_data()
|
|
411
|
+
if not updated_data:
|
|
412
|
+
return
|
|
413
|
+
|
|
414
|
+
# Update item in collection
|
|
415
|
+
success = self.connection.update_items(
|
|
416
|
+
self.current_collection,
|
|
417
|
+
ids=[updated_data["id"]],
|
|
418
|
+
documents=[updated_data["document"]] if updated_data["document"] else None,
|
|
419
|
+
metadatas=[updated_data["metadata"]] if updated_data["metadata"] else None
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
if success:
|
|
423
|
+
QMessageBox.information(self, "Success", "Item updated successfully.")
|
|
424
|
+
self._load_data()
|
|
425
|
+
else:
|
|
426
|
+
QMessageBox.warning(self, "Error", "Failed to update item.")
|
|
427
|
+
|
|
428
|
+
def _export_data(self, format_type: str):
|
|
429
|
+
"""Export collection data to file."""
|
|
430
|
+
if not self.current_collection:
|
|
431
|
+
QMessageBox.warning(self, "No Collection", "Please select a collection first.")
|
|
432
|
+
return
|
|
433
|
+
|
|
434
|
+
# Get all data (not just current page)
|
|
435
|
+
self.loading_dialog.show_loading("Exporting data...")
|
|
436
|
+
QApplication.processEvents()
|
|
437
|
+
|
|
438
|
+
try:
|
|
439
|
+
# Get filter if active
|
|
440
|
+
where_filter = None
|
|
441
|
+
if self.filter_group.isChecked() and self.filter_builder.has_filters():
|
|
442
|
+
where_filter = self.filter_builder.get_filter()
|
|
443
|
+
|
|
444
|
+
# Fetch all data
|
|
445
|
+
all_data = self.connection.get_all_items(
|
|
446
|
+
self.current_collection,
|
|
447
|
+
where=where_filter
|
|
448
|
+
)
|
|
449
|
+
finally:
|
|
450
|
+
self.loading_dialog.hide_loading()
|
|
451
|
+
|
|
452
|
+
if not all_data or not all_data.get("ids"):
|
|
453
|
+
QMessageBox.warning(self, "No Data", "No data to export.")
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
# Select file path
|
|
457
|
+
file_filters = {
|
|
458
|
+
"json": "JSON Files (*.json)",
|
|
459
|
+
"csv": "CSV Files (*.csv)",
|
|
460
|
+
"parquet": "Parquet Files (*.parquet)"
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
file_path, _ = QFileDialog.getSaveFileName(
|
|
464
|
+
self,
|
|
465
|
+
f"Export to {format_type.upper()}",
|
|
466
|
+
f"{self.current_collection}.{format_type}",
|
|
467
|
+
file_filters[format_type]
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
if not file_path:
|
|
471
|
+
return
|
|
472
|
+
|
|
473
|
+
# Export
|
|
474
|
+
service = ImportExportService()
|
|
475
|
+
success = False
|
|
476
|
+
|
|
477
|
+
if format_type == "json":
|
|
478
|
+
success = service.export_to_json(all_data, file_path)
|
|
479
|
+
elif format_type == "csv":
|
|
480
|
+
success = service.export_to_csv(all_data, file_path)
|
|
481
|
+
elif format_type == "parquet":
|
|
482
|
+
success = service.export_to_parquet(all_data, file_path)
|
|
483
|
+
|
|
484
|
+
if success:
|
|
485
|
+
QMessageBox.information(
|
|
486
|
+
self,
|
|
487
|
+
"Export Successful",
|
|
488
|
+
f"Exported {len(all_data['ids'])} items to {file_path}"
|
|
489
|
+
)
|
|
490
|
+
else:
|
|
491
|
+
QMessageBox.warning(self, "Export Failed", "Failed to export data.")
|
|
492
|
+
|
|
493
|
+
def _import_data(self, format_type: str):
|
|
494
|
+
"""Import data from file into collection."""
|
|
495
|
+
if not self.current_collection:
|
|
496
|
+
QMessageBox.warning(self, "No Collection", "Please select a collection first.")
|
|
497
|
+
return
|
|
498
|
+
|
|
499
|
+
# Select file to import
|
|
500
|
+
file_filters = {
|
|
501
|
+
"json": "JSON Files (*.json)",
|
|
502
|
+
"csv": "CSV Files (*.csv)",
|
|
503
|
+
"parquet": "Parquet Files (*.parquet)"
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
file_path, _ = QFileDialog.getOpenFileName(
|
|
507
|
+
self,
|
|
508
|
+
f"Import from {format_type.upper()}",
|
|
509
|
+
"",
|
|
510
|
+
file_filters[format_type]
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
if not file_path:
|
|
514
|
+
return
|
|
515
|
+
|
|
516
|
+
# Import
|
|
517
|
+
self.loading_dialog.show_loading("Importing data...")
|
|
518
|
+
QApplication.processEvents()
|
|
519
|
+
|
|
520
|
+
try:
|
|
521
|
+
service = ImportExportService()
|
|
522
|
+
imported_data = None
|
|
523
|
+
|
|
524
|
+
if format_type == "json":
|
|
525
|
+
imported_data = service.import_from_json(file_path)
|
|
526
|
+
elif format_type == "csv":
|
|
527
|
+
imported_data = service.import_from_csv(file_path)
|
|
528
|
+
elif format_type == "parquet":
|
|
529
|
+
imported_data = service.import_from_parquet(file_path)
|
|
530
|
+
|
|
531
|
+
if not imported_data:
|
|
532
|
+
QMessageBox.warning(self, "Import Failed", "Failed to parse import file.")
|
|
533
|
+
return
|
|
534
|
+
|
|
535
|
+
# Add items to collection
|
|
536
|
+
success = self.connection.add_items(
|
|
537
|
+
self.current_collection,
|
|
538
|
+
documents=imported_data["documents"],
|
|
539
|
+
metadatas=imported_data.get("metadatas"),
|
|
540
|
+
ids=imported_data.get("ids"),
|
|
541
|
+
embeddings=imported_data.get("embeddings")
|
|
542
|
+
)
|
|
543
|
+
finally:
|
|
544
|
+
self.loading_dialog.hide_loading()
|
|
545
|
+
|
|
546
|
+
if success:
|
|
547
|
+
QMessageBox.information(
|
|
548
|
+
self,
|
|
549
|
+
"Import Successful",
|
|
550
|
+
f"Imported {len(imported_data['ids'])} items."
|
|
551
|
+
)
|
|
552
|
+
self._load_data()
|
|
553
|
+
else:
|
|
554
|
+
QMessageBox.warning(self, "Import Failed", "Failed to import data.")
|
|
555
|
+
|