pygpt-net 2.7.7__py3-none-any.whl → 2.7.9__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 (98) hide show
  1. pygpt_net/CHANGELOG.txt +12 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +5 -1
  4. pygpt_net/controller/assistant/batch.py +2 -2
  5. pygpt_net/controller/assistant/files.py +7 -6
  6. pygpt_net/controller/assistant/threads.py +0 -0
  7. pygpt_net/controller/chat/command.py +0 -0
  8. pygpt_net/controller/dialogs/confirm.py +35 -58
  9. pygpt_net/controller/lang/mapping.py +9 -9
  10. pygpt_net/controller/realtime/realtime.py +13 -1
  11. pygpt_net/controller/remote_store/{google/batch.py → batch.py} +209 -252
  12. pygpt_net/controller/remote_store/remote_store.py +982 -13
  13. pygpt_net/core/command/command.py +0 -0
  14. pygpt_net/core/db/viewer.py +1 -1
  15. pygpt_net/core/realtime/worker.py +3 -1
  16. pygpt_net/{controller/remote_store/google → core/remote_store/anthropic}/__init__.py +0 -1
  17. pygpt_net/core/remote_store/anthropic/files.py +211 -0
  18. pygpt_net/core/remote_store/anthropic/store.py +208 -0
  19. pygpt_net/core/remote_store/openai/store.py +5 -4
  20. pygpt_net/core/remote_store/remote_store.py +5 -1
  21. pygpt_net/{controller/remote_store/openai → core/remote_store/xai}/__init__.py +0 -1
  22. pygpt_net/core/remote_store/xai/files.py +225 -0
  23. pygpt_net/core/remote_store/xai/store.py +219 -0
  24. pygpt_net/data/config/config.json +10 -6
  25. pygpt_net/data/config/models.json +38 -22
  26. pygpt_net/data/config/settings.json +54 -1
  27. pygpt_net/data/icons/folder_eye.svg +1 -0
  28. pygpt_net/data/icons/folder_eye_filled.svg +1 -0
  29. pygpt_net/data/icons/folder_open.svg +1 -0
  30. pygpt_net/data/icons/folder_open_filled.svg +1 -0
  31. pygpt_net/data/locale/locale.de.ini +4 -3
  32. pygpt_net/data/locale/locale.en.ini +14 -4
  33. pygpt_net/data/locale/locale.es.ini +4 -3
  34. pygpt_net/data/locale/locale.fr.ini +4 -3
  35. pygpt_net/data/locale/locale.it.ini +4 -3
  36. pygpt_net/data/locale/locale.pl.ini +5 -4
  37. pygpt_net/data/locale/locale.uk.ini +4 -3
  38. pygpt_net/data/locale/locale.zh.ini +4 -3
  39. pygpt_net/icons.qrc +4 -0
  40. pygpt_net/icons_rc.py +282 -138
  41. pygpt_net/provider/api/anthropic/__init__.py +2 -0
  42. pygpt_net/provider/api/anthropic/chat.py +84 -1
  43. pygpt_net/provider/api/anthropic/store.py +307 -0
  44. pygpt_net/provider/api/anthropic/stream.py +75 -0
  45. pygpt_net/provider/api/anthropic/worker/__init__.py +0 -0
  46. pygpt_net/provider/api/anthropic/worker/importer.py +278 -0
  47. pygpt_net/provider/api/google/chat.py +59 -2
  48. pygpt_net/provider/api/google/realtime/client.py +70 -24
  49. pygpt_net/provider/api/google/realtime/realtime.py +48 -12
  50. pygpt_net/provider/api/google/store.py +124 -3
  51. pygpt_net/provider/api/google/stream.py +91 -24
  52. pygpt_net/provider/api/google/worker/importer.py +16 -28
  53. pygpt_net/provider/api/openai/assistants.py +2 -2
  54. pygpt_net/provider/api/openai/realtime/realtime.py +26 -6
  55. pygpt_net/provider/api/openai/store.py +4 -1
  56. pygpt_net/provider/api/openai/worker/importer.py +19 -61
  57. pygpt_net/provider/api/openai/worker/importer_assistants.py +230 -0
  58. pygpt_net/provider/api/x_ai/__init__.py +27 -6
  59. pygpt_net/provider/api/x_ai/audio.py +43 -11
  60. pygpt_net/provider/api/x_ai/chat.py +92 -4
  61. pygpt_net/provider/api/x_ai/realtime/__init__.py +12 -0
  62. pygpt_net/provider/api/x_ai/realtime/client.py +1864 -0
  63. pygpt_net/provider/api/x_ai/realtime/realtime.py +213 -0
  64. pygpt_net/provider/api/x_ai/remote_tools.py +102 -1
  65. pygpt_net/provider/api/x_ai/store.py +610 -0
  66. pygpt_net/provider/api/x_ai/stream.py +30 -9
  67. pygpt_net/provider/api/x_ai/tools.py +51 -0
  68. pygpt_net/provider/api/x_ai/worker/importer.py +308 -0
  69. pygpt_net/provider/audio_input/xai_grok_voice.py +390 -0
  70. pygpt_net/provider/audio_output/xai_tts.py +325 -0
  71. pygpt_net/provider/core/config/patch.py +29 -3
  72. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +2 -2
  73. pygpt_net/provider/core/model/patch.py +49 -1
  74. pygpt_net/tools/image_viewer/tool.py +334 -34
  75. pygpt_net/tools/image_viewer/ui/dialogs.py +317 -21
  76. pygpt_net/ui/dialog/assistant.py +1 -1
  77. pygpt_net/ui/dialog/plugins.py +13 -5
  78. pygpt_net/ui/dialog/remote_store.py +552 -0
  79. pygpt_net/ui/dialogs.py +3 -5
  80. pygpt_net/ui/layout/ctx/ctx_list.py +58 -7
  81. pygpt_net/ui/menu/tools.py +6 -13
  82. pygpt_net/ui/widget/dialog/{remote_store_google.py → remote_store.py} +10 -10
  83. pygpt_net/ui/widget/element/button.py +4 -4
  84. pygpt_net/ui/widget/image/display.py +2 -2
  85. pygpt_net/ui/widget/lists/context.py +2 -2
  86. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/METADATA +14 -2
  87. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/RECORD +87 -75
  88. pygpt_net/controller/remote_store/google/store.py +0 -615
  89. pygpt_net/controller/remote_store/openai/batch.py +0 -524
  90. pygpt_net/controller/remote_store/openai/store.py +0 -699
  91. pygpt_net/ui/dialog/remote_store_google.py +0 -539
  92. pygpt_net/ui/dialog/remote_store_openai.py +0 -539
  93. pygpt_net/ui/widget/dialog/remote_store_openai.py +0 -56
  94. pygpt_net/ui/widget/lists/remote_store_google.py +0 -248
  95. pygpt_net/ui/widget/lists/remote_store_openai.py +0 -317
  96. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/LICENSE +0 -0
  97. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/WHEEL +0 -0
  98. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/entry_points.txt +0 -0
@@ -6,30 +6,999 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2026.01.02 19:00:00 #
9
+ # Updated Date: 2026.01.06 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from .openai import OpenAIRemoteStore
13
- from .google import GoogleRemoteStore
12
+ import copy
13
+ import json
14
+ from typing import Optional, Union, Any, List
15
+
16
+ from PySide6.QtCore import QTimer
17
+ from PySide6.QtWidgets import QApplication, QMessageBox
18
+ from PySide6.QtGui import QStandardItem
19
+
20
+ from pygpt_net.item.store import RemoteStoreItem
21
+ from pygpt_net.utils import trans
22
+
23
+ from .batch import Batch
24
+
14
25
 
15
26
  class RemoteStore:
27
+
28
+ DEFAULT_PROVIDER = "openai"
29
+ PROVIDERS = {
30
+ "openai": "OpenAI",
31
+ "google": "Google",
32
+ # "anthropic": "Anthropic", # TODO: enable when SDK fixed
33
+ "xai": "xAI (Collections)",
34
+ }
35
+
16
36
  def __init__(self, window=None):
17
37
  """
18
- Assistant controller
38
+ Unified Remote Store controller
19
39
 
20
40
  :param window: Window instance
21
41
  """
22
42
  self.window = window
23
- self.openai = OpenAIRemoteStore(window)
24
- self.google = GoogleRemoteStore(window)
43
+
44
+ # Unified UI state
45
+ self.dialog = False
46
+ self.config_initialized = False
47
+ self.current: Optional[str] = None
48
+ self.width = 900
49
+ self.height = 560
50
+ self.id = "remote_store"
51
+ self.initialized = False
52
+
53
+ # Active provider: 'google' | 'openai' | 'anthropic' | 'xai'
54
+ self.provider_key: Optional[str] = None
55
+
56
+ # Mapping row -> ids for current stores/files list
57
+ self._stores_row_to_id: List[str] = []
58
+ self._files_row_to_id: List[str] = []
59
+
60
+ # Upload queue
61
+ self.files_to_upload: List[str] = []
62
+
63
+ # Unified options schema; UI hides provider-specific fields
64
+ self.options = {
65
+ "id": {
66
+ "type": "text",
67
+ "label": "remote_store.id",
68
+ "description": "remote_store.id.description",
69
+ "read_only": True,
70
+ "value": "",
71
+ },
72
+ "name": {
73
+ "type": "text",
74
+ "label": "remote_store.name",
75
+ "value": "",
76
+ },
77
+ "expire_days": {
78
+ "type": "int",
79
+ "label": "remote_store.expire_days",
80
+ "value": 0, # OpenAI only; hidden for Google/Anthropic/xAI
81
+ "advanced": False,
82
+ },
83
+ "status": {
84
+ "type": "textarea",
85
+ "label": "remote_store.status",
86
+ "read_only": True,
87
+ "value": "",
88
+ },
89
+ }
90
+
91
+ self.batch = Batch(self)
92
+
93
+ # ======================== Provider helpers ========================
94
+
95
+ def get_providers(self) -> dict:
96
+ """Return available providers."""
97
+ return self.PROVIDERS
98
+
99
+ def get_provider_keys(self) -> List[str]:
100
+ """Return available provider keys."""
101
+ return list(self.PROVIDERS.keys())
102
+
103
+ def _get_provider(self) -> str:
104
+ """Return current provider key."""
105
+ if self.provider_key in self.get_provider_keys():
106
+ return self.provider_key
107
+ key = self.window.core.config.get("remote_store.provider")
108
+ if key not in self.get_provider_keys():
109
+ key = self.DEFAULT_PROVIDER
110
+ try:
111
+ self.window.core.config.set("remote_store.provider", key)
112
+ except Exception:
113
+ pass
114
+ self.provider_key = key
115
+ return key
116
+
117
+ def set_provider(self, key: str):
118
+ """Set active provider and re-init UI."""
119
+ if key not in self.get_provider_keys():
120
+ return
121
+ self.provider_key = key
122
+ try:
123
+ self.window.core.config.set("remote_store.provider", key)
124
+ except Exception:
125
+ pass
126
+ # Force selecting the first visible store for the new provider and refresh files list
127
+ self.init(select_first=True)
128
+
129
+ def _core_for(self, provider: Optional[str] = None):
130
+ key = provider or self._get_provider()
131
+ if hasattr(self.window.core.remote_store, key):
132
+ return getattr(self.window.core.remote_store, key)
133
+
134
+ def _files_core_for(self, provider: Optional[str] = None):
135
+ return self._core_for(provider).files
136
+
137
+ def _api_store_for(self, provider: Optional[str] = None):
138
+ key = provider or self._get_provider()
139
+ if hasattr(self.window.core.api, key):
140
+ return getattr(self.window.core.api, key).store
141
+
142
+ # ======================== Provider hooks ========================
143
+
144
+ def after_create(self, provider: str, store: RemoteStoreItem):
145
+ """Hook: called after store creation (per provider)."""
146
+ if provider == "openai":
147
+ self.window.controller.assistant.editor.update_store_list() # update stores list in assistant dialog
148
+
149
+ def after_delete(self, provider: str, store_id: str):
150
+ """Hook: called after store deletion (per provider)."""
151
+ if provider == "openai":
152
+ self.window.controller.assistant.editor.update_store_list() # update stores list in assistant dialog
153
+
154
+ def after_update(self, provider: str, store: RemoteStoreItem):
155
+ """Hook: called after store update (per provider)."""
156
+ if provider == "openai":
157
+ self.window.controller.assistant.editor.update_store_list()
158
+
159
+ def after_truncated_stores(self, provider: str):
160
+ """Hook: called after truncating all stores (per provider)."""
161
+ if provider == "openai":
162
+ try:
163
+ self.window.controller.assistant.batch.remove_all_stores_from_assistants()
164
+ self.window.controller.assistant.editor.update_store_list() # update stores list in assistant dialog
165
+ except Exception:
166
+ pass
167
+
168
+ def after_imported_stores(self, provider: str):
169
+ """Hook: called after importing stores (per provider)."""
170
+ if provider == "openai":
171
+ try:
172
+ self.window.controller.assistant.files.update()
173
+ self.window.controller.assistant.editor.update_store_list() # update stores list in assistant dialog
174
+ except Exception:
175
+ pass
176
+
177
+ # ======================== Options ========================
178
+
179
+ def get_options(self) -> dict:
180
+ return self.options
181
+
182
+ def get_option(self, key: str) -> Optional[dict]:
183
+ if key in self.options:
184
+ return self.options[key]
185
+
186
+ # ======================== Lifecycle ========================
25
187
 
26
188
  def setup(self):
27
- """Setup stores"""
28
- self.window.core.remote_store.openai.load_all()
29
- self.window.core.remote_store.google.load_all()
189
+ """Setup caches and build the unified dialog."""
190
+ try:
191
+ self.window.core.remote_store.openai.load_all()
192
+ self.window.core.remote_store.google.load_all()
193
+ self.window.core.remote_store.anthropic.load_all()
194
+ self.window.core.remote_store.xai.load_all()
195
+ except Exception as e:
196
+ self.window.core.debug.log(e)
30
197
 
31
198
  def reload(self):
32
- """Reload stores"""
33
- self.setup()
34
- self.window.controller.remote_store.openai.reset()
35
- self.window.controller.remote_store.google.reset()
199
+ """Reload provider data and refresh unified view."""
200
+ try:
201
+ self.window.core.remote_store.openai.load_all()
202
+ self.window.core.remote_store.google.load_all()
203
+ self.window.core.remote_store.anthropic.load_all()
204
+ self.window.core.remote_store.xai.load_all()
205
+ except Exception as e:
206
+ self.window.core.debug.log(e)
207
+ self.reset()
208
+
209
+ def reset(self):
210
+ """Reset current selection."""
211
+ self.current = None
212
+ if self.dialog:
213
+ self.init()
214
+
215
+ def toggle_editor(self, provider: Optional[str] = None):
216
+ """Toggle unified dialog."""
217
+ if provider is not None:
218
+ self.set_provider(provider)
219
+ if self.dialog:
220
+ self.close()
221
+ else:
222
+ self.open()
223
+
224
+ def open(self, force: bool = False):
225
+ """Open dialog."""
226
+ if not self.config_initialized:
227
+ self.setup()
228
+ self.config_initialized = True
229
+ if not self.dialog or force:
230
+ # Force selecting the first visible store on dialog open
231
+ self.init(select_first=True)
232
+ self.window.ui.dialogs.open(
233
+ "remote_store",
234
+ width=self.width,
235
+ height=self.height,
236
+ )
237
+ self.dialog = True
238
+
239
+ def close(self):
240
+ """Close dialog."""
241
+ if self.dialog:
242
+ self.window.ui.dialogs.close("remote_store")
243
+ self.dialog = False
244
+
245
+ # ======================== Initialize / Refresh ========================
246
+
247
+ def init(self, select_first: bool = False):
248
+ """Initialize UI state for the active provider."""
249
+ if not self.initialized:
250
+ # Setup dialog UI
251
+ self.window.remote_store.setup()
252
+
253
+ # Ensure provider combobox is synced at first load
254
+ try:
255
+ self.window.remote_store.set_provider_in_ui(self._get_provider())
256
+ except Exception:
257
+ pass
258
+ self.initialized = True
259
+
260
+ self.reload_items()
261
+
262
+ # Ensure a valid selection, optionally forcing the first visible store
263
+ core = self._core_for()
264
+ if select_first:
265
+ # Reset current to enforce picking the first visible store below
266
+ self.current = None
267
+ if self.current is None or not core.has(self.current):
268
+ self.current = self.get_first_visible()
269
+
270
+ options = copy.deepcopy(self.get_options())
271
+ if self.current is not None and core.has(self.current):
272
+ store = core.items[self.current]
273
+ data_dict = store.to_dict()
274
+ for key in options:
275
+ if key in data_dict:
276
+ value = data_dict[key]
277
+ if key == "status":
278
+ options[key]["value"] = json.dumps(value, indent=4)
279
+ else:
280
+ options[key]["value"] = value
281
+ # Reflect selection in the left list
282
+ self.set_tab_by_id(self.current)
283
+ self.window.controller.config.load_options(self.id, options)
284
+ else:
285
+ self.current = None
286
+ self.window.controller.config.load_options(self.id, options)
287
+
288
+ # Provider-specific visibility
289
+ try:
290
+ self.window.remote_store.sync_provider_dependent_ui(self._get_provider())
291
+ except Exception:
292
+ pass
293
+
294
+ # Always refresh files list on provider/selection changes (also clears when empty)
295
+ self.update_files_list()
296
+ try:
297
+ self.window.remote_store.sync_hide_threads_checkbox(self._get_provider())
298
+ except Exception:
299
+ pass
300
+
301
+ # Update dialog title
302
+ try:
303
+ self.window.remote_store.update_title(self._dialog_title_for_provider(self._get_provider()))
304
+ except Exception:
305
+ pass
306
+
307
+ def refresh_status(self):
308
+ """Reload current store status from API and refresh UI."""
309
+ if self.current is None:
310
+ return
311
+ core = self._core_for()
312
+ if not core.has(self.current):
313
+ return
314
+
315
+ self.window.update_status(trans('status.sending'))
316
+ QApplication.processEvents()
317
+ try:
318
+ core.update_status(self.current)
319
+ store = core.items[self.current]
320
+ core.update(store)
321
+ self.window.update_status(trans('status.finished'))
322
+ except Exception as e:
323
+ self.window.core.debug.log(e)
324
+ self.window.update_status(trans('status.error'))
325
+
326
+ self.update_current()
327
+ self.update()
328
+ self.update_files_list()
329
+
330
+ def refresh_store(self, store: RemoteStoreItem, update: bool = True, provider: Optional[str] = None):
331
+ """Refresh a store by item."""
332
+ if provider:
333
+ if self._get_provider() != provider:
334
+ return
335
+ provider = self._get_provider()
336
+ core = self._core_for(provider)
337
+ core.update_status(store.id)
338
+ core.update(store)
339
+ if update and store.id == self.current:
340
+ self.update_current()
341
+
342
+ def refresh_by_idx(self, idx: Union[int, list]):
343
+ """Refresh store(s) by list index or list of indexes."""
344
+ store_ids = []
345
+ ids = idx if isinstance(idx, list) else [idx]
346
+ for i in ids:
347
+ store_id = self.get_by_tab_idx(i)
348
+ if store_id is not None:
349
+ store_ids.append(store_id)
350
+ self.refresh_by_store_id(store_ids)
351
+
352
+ def refresh_by_store_id(self, store_id: Union[str, list], provider: Optional[str] = None):
353
+ """Refresh store(s) by ID."""
354
+ if provider:
355
+ if self._get_provider() != provider:
356
+ return
357
+ ids = store_id if isinstance(store_id, list) else [store_id]
358
+ updated = False
359
+ is_current = False
360
+ core = self._core_for()
361
+ for sid in ids:
362
+ if sid is not None and core.has(sid):
363
+ store = core.items[sid]
364
+ if store is not None:
365
+ self.window.update_status(trans('status.sending'))
366
+ QApplication.processEvents()
367
+ self.refresh_store(store)
368
+ updated = True
369
+ if self.current == sid:
370
+ is_current = True
371
+ if updated:
372
+ self.window.update_status(trans('status.finished'))
373
+ self.update()
374
+ if is_current:
375
+ self.update_files_list()
376
+
377
+ def refresh_by_store_id_provider(self, provider: str, store_id: Optional[str]):
378
+ """Refresh status for a specific provider+store (used by proxies after async)."""
379
+ if store_id is None:
380
+ return
381
+ core = self._core_for(provider)
382
+ if store_id not in core.items:
383
+ return
384
+ store = core.items[store_id]
385
+ try:
386
+ core.update_status(store_id)
387
+ core.update(store)
388
+ except Exception as e:
389
+ self.window.core.debug.log(e)
390
+
391
+ if provider == self._get_provider() and self.current == store_id:
392
+ self.update_current()
393
+ self.update()
394
+ self.update_files_list()
395
+
396
+ # ======================== UI update helpers ========================
397
+
398
+ def update_current(self):
399
+ """Update current store values in right panel."""
400
+ if self.current is None:
401
+ return
402
+ core = self._core_for()
403
+ if not core.has(self.current):
404
+ return
405
+ store = core.items[self.current]
406
+
407
+ option = copy.deepcopy(self.get_option("status"))
408
+ option["value"] = json.dumps(store.status, indent=4)
409
+ self.window.controller.config.apply(self.id, "status", option)
410
+
411
+ option = copy.deepcopy(self.get_option("name"))
412
+ option["value"] = store.name
413
+ self.window.controller.config.apply(self.id, "name", option)
414
+
415
+ option = copy.deepcopy(self.get_option("id"))
416
+ option["value"] = store.id
417
+ self.window.controller.config.apply(self.id, "id", option)
418
+
419
+ if self._get_provider() == "openai":
420
+ try:
421
+ option = copy.deepcopy(self.get_option("expire_days"))
422
+ option["value"] = int(getattr(store, "expire_days", 0) or 0)
423
+ self.window.controller.config.apply(self.id, "expire_days", option)
424
+ except Exception:
425
+ pass
426
+
427
+ def update(self):
428
+ """Refresh lists and auxiliary widgets."""
429
+ self.reload_items()
430
+ try:
431
+ self.after_update(self._get_provider())
432
+ except Exception:
433
+ pass
434
+ self.update_files_list()
435
+
436
+ def reload_items(self):
437
+ """Reload left stores list for current provider."""
438
+ core = self._core_for()
439
+ items = core.get_all()
440
+ hide_threads = bool(self._get_hide_threads_config(self._get_provider()))
441
+ suffix = trans("remote_store.files.suffix")
442
+
443
+ pairs: List[tuple[str, str]] = []
444
+ for sid, store in items.items():
445
+ if core.is_hidden(sid) or (hide_threads and (store.name is None or store.name == "")):
446
+ continue
447
+ num_files = store.get_file_count()
448
+ name = store.name or store.id
449
+ extras = []
450
+ if num_files > 0:
451
+ extras.append(f"{num_files} {suffix}")
452
+ if getattr(store, "usage_bytes", 0) > 0:
453
+ try:
454
+ extras.append(self.window.core.filesystem.sizeof_fmt(store.usage_bytes))
455
+ except Exception:
456
+ pass
457
+ if extras:
458
+ name = f"{name} ({' - '.join(extras)})"
459
+ pairs.append((sid, name))
460
+
461
+ self._stores_row_to_id = [sid for sid, _ in pairs]
462
+ try:
463
+ self.window.remote_store.update_list_pairs("remote_store.list", pairs)
464
+ except Exception:
465
+ pass
466
+
467
+ self.restore_selection()
468
+
469
+ def restore_selection(self):
470
+ """Restore left selection to self.current."""
471
+ if self.current is not None:
472
+ idx = self.get_tab_by_id(self.current)
473
+ if idx is not None:
474
+ self.set_by_tab(idx)
475
+
476
+ # ======================== Selection ========================
477
+
478
+ def select(self, idx: int):
479
+ """Select store by index in the left list."""
480
+ self.save(persist=False)
481
+ sid = self.get_by_tab_idx(idx)
482
+ if sid is None:
483
+ return
484
+ self.current = sid
485
+ self.init()
486
+ self.update_files_list()
487
+
488
+ def set_by_tab(self, idx: int):
489
+ """Select row (left list) by index."""
490
+ try:
491
+ self.window.remote_store.set_current_row("remote_store.list", idx)
492
+ except Exception:
493
+ pass
494
+
495
+ def set_tab_by_id(self, store_id: str):
496
+ """Select row by store id."""
497
+ idx = self.get_tab_idx(store_id)
498
+ if idx is None:
499
+ return
500
+ self.set_by_tab(idx)
501
+
502
+ def get_tab_idx(self, store_id: str) -> Optional[int]:
503
+ """Return row index for a given store id."""
504
+ try:
505
+ return self._stores_row_to_id.index(store_id)
506
+ except Exception:
507
+ return None
508
+
509
+ def get_tab_by_id(self, store_id: str) -> Optional[int]:
510
+ return self.get_tab_idx(store_id)
511
+
512
+ def get_by_tab_idx(self, idx: int) -> Optional[str]:
513
+ """Return store id by row index."""
514
+ if idx < 0 or idx >= len(self._stores_row_to_id):
515
+ return None
516
+ return self._stores_row_to_id[idx]
517
+
518
+ def get_first_visible(self) -> Optional[str]:
519
+ """Return first visible store id for current provider (respects current UI filters)."""
520
+ # Prefer what is actually visible in the left list
521
+ if self._stores_row_to_id:
522
+ return self._stores_row_to_id[0]
523
+ # Fallback to provider core when left list is not initialized
524
+ core = self._core_for()
525
+ for sid in core.get_ids():
526
+ if not core.is_hidden(sid):
527
+ return sid
528
+ return None
529
+
530
+ # ======================== CRUD: stores ========================
531
+
532
+ def save_btn(self):
533
+ """Save and refresh status."""
534
+ self.window.update_status("Saving...")
535
+ self.save()
536
+ self.refresh_status()
537
+ self.window.update_status("Saved.")
538
+
539
+ def save(self, persist: bool = True):
540
+ """Persist right panel data to store and remote if supported."""
541
+ if self.current is not None and self._core_for().has(self.current):
542
+ current = self._core_for().items[self.current].to_dict()
543
+ options = copy.deepcopy(self.get_options())
544
+ data_dict = current
545
+ for key in options:
546
+ if key == "status":
547
+ data_dict[key] = current.get("status", {})
548
+ continue
549
+ value = self.window.controller.config.get_value(
550
+ parent_id="remote_store",
551
+ key=key,
552
+ option=options[key],
553
+ )
554
+ data_dict[key] = value
555
+ self._core_for().items[self.current].from_dict(data_dict)
556
+
557
+ if persist:
558
+ self.window.update_status(trans('status.sending'))
559
+ QApplication.processEvents()
560
+ if self.current is not None:
561
+ store = self._core_for().update(self._core_for().items[self.current])
562
+ if store is None:
563
+ self.window.update_status(trans('status.error'))
564
+ self.window.ui.dialogs.alert("Failed to save store")
565
+ return
566
+ self.update()
567
+ self.window.update_status(trans("info.settings.saved"))
568
+ self.restore_selection()
569
+ self.update_files_list()
570
+
571
+ def new(self, name: str = "", force: bool = False):
572
+ """Create a new store for active provider."""
573
+ if not force:
574
+ self.window.ui.dialog['create'].id = 'remote_store.new'
575
+ self.window.ui.dialog['create'].input.setText("New vector store")
576
+ self.window.ui.dialog['create'].current = "New vector store"
577
+ self.window.ui.dialog['create'].show()
578
+ return
579
+
580
+ self.window.ui.dialog['create'].close()
581
+ self.window.update_status(trans('status.sending'))
582
+ QApplication.processEvents()
583
+ try:
584
+ core = self._core_for()
585
+ display_name = name or "New vector store"
586
+ store = core.create(display_name)
587
+ if store is None:
588
+ raise RuntimeError("Failed to create new store")
589
+ core.update(store)
590
+
591
+ self.after_create(self._get_provider(), store)
592
+ self.window.update_status(trans('status.finished'))
593
+
594
+ self.current = store.id
595
+ self.reload_items()
596
+ self.set_tab_by_id(self.current)
597
+ self.init()
598
+ self.restore_selection()
599
+ self.refresh_by_store_id(store.id)
600
+ self.update_files_list()
601
+
602
+ except Exception as e:
603
+ self.window.core.debug.log(e)
604
+ self.window.ui.dialogs.alert(str(e))
605
+ self.window.update_status(trans('status.error'))
606
+
607
+ def delete_by_idx(self, idx: Union[int, list], force: bool = False):
608
+ """Delete store(s) by index(es)."""
609
+ store_ids = []
610
+ ids = idx if isinstance(idx, list) else [idx]
611
+ for i in ids:
612
+ store_id = self.get_by_tab_idx(i)
613
+ if store_id is not None:
614
+ store_ids.append(store_id)
615
+ self.delete(store_ids, force)
616
+
617
+ def delete(self, store_id: Optional[Union[str, list]] = None, force: bool = False):
618
+ """Delete store(s) by id."""
619
+ if store_id is None:
620
+ self.window.ui.dialogs.alert("Please select store first.")
621
+ return
622
+
623
+ if not force:
624
+ t = "remote_store.delete"
625
+ self.window.ui.dialogs.confirm(
626
+ type=t,
627
+ id=store_id,
628
+ msg=trans("dialog.remote_store.delete.confirm"),
629
+ )
630
+ return
631
+
632
+ self.window.update_status(trans('status.sending'))
633
+ updated = False
634
+ QApplication.processEvents()
635
+ core = self._core_for()
636
+ ids = store_id if isinstance(store_id, list) else [store_id]
637
+ for sid in ids:
638
+ if self.current == sid:
639
+ self.current = None
640
+ try:
641
+ if core.delete(sid):
642
+ try:
643
+ self.window.controller.assistant.batch.remove_store_from_assistants(sid)
644
+ except Exception:
645
+ pass
646
+ self.window.update_status(trans('status.deleted'))
647
+ core.save()
648
+ self.after_delete(self._get_provider(), sid)
649
+ updated = True
650
+ else:
651
+ self.window.update_status(trans('status.error'))
652
+ except Exception as e:
653
+ self.window.update_status(trans('status.error'))
654
+ self.window.ui.dialogs.alert(e)
655
+ if updated:
656
+ self.update()
657
+ self.init()
658
+ self.restore_selection()
659
+ self.update_files_list()
660
+
661
+ # ======================== Files (documents) ========================
662
+
663
+ def update_files_list(self):
664
+ """Populate files list (documents) for the current store from local DB."""
665
+ model_id = 'remote_store.files.list'
666
+ if model_id not in self.window.ui.models:
667
+ return
668
+ model = self.window.ui.models[model_id]
669
+ try:
670
+ model.removeRows(0, model.rowCount())
671
+ except Exception:
672
+ pass
673
+
674
+ self._files_row_to_id = []
675
+
676
+ if self.current is None:
677
+ return
678
+
679
+ files_db = self._files_core_for()
680
+ if files_db is None:
681
+ return
682
+
683
+ try:
684
+ store_files = files_db.get_by_store_or_thread(self.current, None) or {}
685
+ except Exception as e:
686
+ self.window.core.debug.log(e)
687
+ store_files = {}
688
+
689
+ i = 0
690
+ for file_id, file_obj in store_files.items():
691
+ if isinstance(file_obj, dict):
692
+ data = file_obj
693
+ else:
694
+ data = {}
695
+ for key in ('id', 'file_id', 'name', 'filename', 'bytes', 'size', 'usage_bytes', 'status'):
696
+ try:
697
+ if hasattr(file_obj, key):
698
+ data[key] = getattr(file_obj, key)
699
+ except Exception:
700
+ pass
701
+ if not data and hasattr(file_obj, 'to_dict'):
702
+ try:
703
+ data = file_obj.to_dict()
704
+ except Exception:
705
+ data = {}
706
+
707
+ name = data.get('name') or data.get('filename') or file_id
708
+ size_val = None
709
+ for k in ('bytes', 'size', 'usage_bytes'):
710
+ if data.get(k) is not None:
711
+ size_val = data.get(k)
712
+ break
713
+
714
+ size_txt = ""
715
+ try:
716
+ if size_val:
717
+ size_txt = self.window.core.filesystem.sizeof_fmt(int(size_val))
718
+ except Exception:
719
+ pass
720
+
721
+ extra = []
722
+ if size_txt:
723
+ extra.append(size_txt)
724
+ if data.get('status'):
725
+ extra.append(str(data.get('status')))
726
+ label = name
727
+ if extra:
728
+ label += " ({})".format(", ".join(extra))
729
+
730
+ item = QStandardItem(label)
731
+ item.setEditable(False)
732
+ model.setItem(i, 0, item)
733
+ self._files_row_to_id.append(data['file_id'] if 'file_id' in data else file_id)
734
+ i += 1
735
+
736
+ def delete_file_by_idx(self, idx: Union[int, list], force: bool = False):
737
+ """
738
+ Delete a single document from the current store.
739
+ Uses inline confirmation if force is False.
740
+ """
741
+ if self.current is None:
742
+ self.window.ui.dialogs.alert("Please select store first.")
743
+ return
744
+
745
+ model_id = 'remote_store.files.list'
746
+ if model_id not in self.window.ui.models:
747
+ return
748
+
749
+ if not isinstance(idx, list):
750
+ idx = [idx]
751
+ idx_list = []
752
+ for i in idx:
753
+ if i < 0 or i >= len(self._files_row_to_id):
754
+ continue
755
+ idx_list.append(i)
756
+
757
+ file_ids = [self._files_row_to_id[i] for i in idx_list if self._files_row_to_id[i]]
758
+ if not file_ids:
759
+ return
760
+
761
+ if not force:
762
+ m = QMessageBox(self.window)
763
+ m.setIcon(QMessageBox.Warning)
764
+ m.setWindowTitle(trans("remote_store.menu.file.delete"))
765
+ m.setText(trans('confirm.remote_store.file.delete'))
766
+ m.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
767
+ m.setDefaultButton(QMessageBox.No)
768
+ if m.exec() != QMessageBox.Yes:
769
+ return
770
+
771
+ self._delete_file_by_idx_provider(idx, self._get_provider())
772
+
773
+ def _delete_file_by_idx_provider(self, idx: Union[int, list], provider: str, file_id: Optional[str] = None):
774
+ """Provider-explicit file delete (used by proxies/confirm)."""
775
+ if self.current is None and provider == self._get_provider():
776
+ self.window.ui.dialogs.alert(trans("dialog.remote_store.alert.select"))
777
+ return
778
+
779
+ model_id = 'remote_store.files.list'
780
+ if model_id not in self.window.ui.models and provider == self._get_provider():
781
+ return
782
+
783
+ idx_list = []
784
+ if file_id is None:
785
+ if not isinstance(idx, list):
786
+ idx = [idx]
787
+ for i in idx:
788
+ if i < 0 or i >= len(self._files_row_to_id):
789
+ continue
790
+ idx_list.append(i)
791
+
792
+ if not idx_list:
793
+ return
794
+
795
+ file_ids = [self._files_row_to_id[i] for i in idx_list if self._files_row_to_id[i]]
796
+ if not file_ids:
797
+ return
798
+ else:
799
+ file_ids = [file_id]
800
+ idx_list = [idx] if isinstance(idx, int) else idx
801
+
802
+ # sort indexes descending to avoid shifting issues when removing multiple
803
+ idx_list.sort(reverse=True)
804
+
805
+ self.window.update_status(trans('status.sending'))
806
+ QApplication.processEvents()
807
+ try:
808
+ api = self._api_store_for(provider)
809
+ removed = False
810
+
811
+ for file_id in file_ids:
812
+ if hasattr(api, 'remove_store_file'):
813
+ try:
814
+ api.remove_store_file(self.current if provider == self._get_provider() else "", file_id)
815
+ removed = True
816
+ except Exception as e:
817
+ self.window.core.debug.log(e)
818
+
819
+ if not removed and hasattr(api, 'remove_file'):
820
+ try:
821
+ api.remove_file(file_id)
822
+ removed = True
823
+ except Exception as e:
824
+ self.window.core.debug.log(e)
825
+
826
+ if not removed:
827
+ raise RuntimeError("Remove file API not available.")
828
+
829
+ for file_id in file_ids:
830
+ try:
831
+ self._files_core_for(provider).delete_by_file_id(file_id)
832
+ except Exception as e:
833
+ self.window.core.debug.log(e)
834
+
835
+ if provider == self._get_provider():
836
+ try:
837
+ for i in idx_list:
838
+ self.window.ui.models[model_id].removeRow(i)
839
+ try:
840
+ del self._files_row_to_id[i]
841
+ except Exception:
842
+ pass
843
+ except Exception:
844
+ pass
845
+
846
+ QTimer.singleShot(1200, self.refresh_status)
847
+ self.window.update_status(trans('status.deleted'))
848
+
849
+ except Exception as e:
850
+ self.window.core.debug.log(e)
851
+ self.window.ui.dialogs.alert("Failed to delete file: {}".format(e))
852
+ self.window.update_status(trans('status.error'))
853
+ if provider == self._get_provider():
854
+ self.update_files_list()
855
+
856
+ # ======================== Hide threads ========================
857
+
858
+ def set_hide_thread(self, state: bool):
859
+ """Toggle hide thread stores for current provider."""
860
+ self._set_hide_threads_config(self._get_provider(), state)
861
+ self.update()
862
+
863
+ def _get_hide_threads_config(self, provider: str) -> Any:
864
+ key = "remote_store.hide_threads"
865
+ return self.window.core.config.get(key)
866
+
867
+ def _set_hide_threads_config(self, provider: str, state: bool):
868
+ key = "remote_store.hide_threads"
869
+ self.window.core.config.set(key, bool(state))
870
+
871
+ # ======================== Title ========================
872
+
873
+ def _dialog_title_for_provider(self, provider: str) -> str:
874
+ return trans('dialog.remote_store')
875
+
876
+ # ======================== Unified batch entry points (UI menu) ========================
877
+
878
+ def import_stores(self, force: bool = False):
879
+ if not force:
880
+ t = 'remote_store.import'
881
+ self.window.ui.dialogs.confirm(type=t, id='', msg=trans('confirm.remote_store.import'))
882
+ return
883
+ imp = self._importer_for(self._get_provider())
884
+ self._core_for().truncate()
885
+ imp.import_vector_stores()
886
+
887
+ def refresh_stores(self, force: bool = False):
888
+ if not force:
889
+ t = 'remote_store.refresh'
890
+ self.window.ui.dialogs.confirm(type=t, id='', msg=trans('confirm.remote_store.refresh'))
891
+ return
892
+ imp = self._importer_for(self._get_provider())
893
+ imp.refresh_vector_stores()
894
+
895
+ def truncate_stores(self, force: bool = False):
896
+ if not force:
897
+ t = 'remote_store.truncate'
898
+ self.window.ui.dialogs.confirm(type=t, id='', msg=trans('confirm.remote_store.truncate'))
899
+ return
900
+ imp = self._importer_for(self._get_provider())
901
+ self._core_for().truncate()
902
+ imp.truncate_vector_stores()
903
+
904
+ def import_files(self, force: bool = False):
905
+ if not force:
906
+ t = 'remote_store.files.import.all'
907
+ self.window.ui.dialogs.confirm(type=t, id='', msg=trans('confirm.remote_store.import_files'))
908
+ return
909
+ imp = self._importer_for(self._get_provider())
910
+ imp.import_files()
911
+
912
+ def import_store_files(self, store_id: str, force: bool = False):
913
+ if store_id is None:
914
+ self.window.ui.dialogs.alert(trans("dialog.remote_store.alert.select"))
915
+ return
916
+ if not force:
917
+ t = 'remote_store.files.import.store'
918
+ self.window.ui.dialogs.confirm(type=t, id=store_id, msg=trans('confirm.remote_store.import_files.store'))
919
+ return
920
+ imp = self._importer_for(self._get_provider())
921
+ self.window.update_status("Importing files...please wait...")
922
+ imp.import_files(store_id)
923
+
924
+ def truncate_files(self, force: bool = False):
925
+ if not force:
926
+ t = 'remote_store.files.truncate'
927
+ key = 'confirm.remote_store.files.truncate'
928
+ self.window.ui.dialogs.confirm(type=t, id='', msg=trans(key))
929
+ return
930
+ imp = self._importer_for(self._get_provider())
931
+ imp.truncate_files()
932
+
933
+ def truncate_store_files(self, store_id: Union[str, list], force: bool = False):
934
+ if store_id is None:
935
+ self.window.ui.dialogs.alert(trans("dialog.remote_store.alert.select"))
936
+ return
937
+ if not force:
938
+ t = 'remote_store.files.truncate.store'
939
+ key = 'confirm.remote_store.files.truncate.store'
940
+ self.window.ui.dialogs.confirm(type=t, id=store_id, msg=trans(key))
941
+ return
942
+ imp = self._importer_for(self._get_provider())
943
+ ids = store_id if isinstance(store_id, list) else [store_id]
944
+ for sid in ids:
945
+ imp.truncate_files(sid)
946
+
947
+ def clear_files(self, force: bool = False):
948
+ if not force:
949
+ t = 'remote_store.files.clear.all'
950
+ self.window.ui.dialogs.confirm(type=t, id='', msg=trans('confirm.assistant.files.clear'))
951
+ return
952
+ self.window.update_status("Clearing documents...please wait...")
953
+ QApplication.processEvents()
954
+ try:
955
+ self._files_core_for().truncate_local()
956
+ except Exception as e:
957
+ self.window.core.debug.log(e)
958
+ self.update()
959
+ self.window.update_status("OK. All documents cleared.")
960
+ self.window.ui.dialogs.alert(trans("status.finished"))
961
+
962
+ def clear_store_files(self, store_id: Optional[Union[str, list]] = None, force: bool = False):
963
+ if store_id is None:
964
+ self.window.ui.dialogs.alert(trans("dialog.remote_store.alert.select"))
965
+ return
966
+ if not force:
967
+ t = 'remote_store.files.clear.store'
968
+ self.window.ui.dialogs.confirm(type=t, id=store_id, msg=trans('confirm.assistant.files.clear'))
969
+ return
970
+ self.window.update_status("Clearing store documents...please wait...")
971
+ QApplication.processEvents()
972
+ ids = store_id if isinstance(store_id, list) else [store_id]
973
+ for sid in ids:
974
+ try:
975
+ self._files_core_for().truncate_local(sid)
976
+ except Exception as e:
977
+ self.window.core.debug.log(e)
978
+ self.update()
979
+ self.window.update_status("OK. Store documents cleared.")
980
+ self.window.ui.dialogs.alert(trans("status.finished"))
981
+
982
+ def clear_stores(self, force: bool = False):
983
+ if not force:
984
+ t = 'remote_store.clear'
985
+ self.window.ui.dialogs.confirm(type=t, id='', msg=trans('confirm.remote_store.clear'))
986
+ return
987
+ self.window.update_status("Clearing stores...please wait...")
988
+ QApplication.processEvents()
989
+ try:
990
+ self._core_for().truncate()
991
+ except Exception as e:
992
+ self.window.core.debug.log(e)
993
+ self.update()
994
+ self.current = None
995
+ self.init()
996
+ self.window.update_status("OK. All stores cleared.")
997
+
998
+ # ======================== Utilities ========================
999
+
1000
+ def _importer_for(self, provider: str):
1001
+ if (hasattr(self.window.core.api, provider)
1002
+ and hasattr(getattr(self.window.core.api, provider), 'store')
1003
+ and hasattr(getattr(self.window.core.api, provider).store, 'importer')):
1004
+ return getattr(self.window.core.api, provider).store.importer