pygpt-net 2.6.67__py3-none-any.whl → 2.7.1__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 (78) hide show
  1. pygpt_net/CHANGELOG.txt +20 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/assistant/assistant.py +13 -8
  4. pygpt_net/controller/assistant/batch.py +29 -15
  5. pygpt_net/controller/assistant/files.py +19 -14
  6. pygpt_net/controller/assistant/store.py +63 -41
  7. pygpt_net/controller/attachment/attachment.py +45 -35
  8. pygpt_net/controller/chat/attachment.py +50 -39
  9. pygpt_net/controller/config/field/dictionary.py +26 -14
  10. pygpt_net/controller/ctx/common.py +27 -17
  11. pygpt_net/controller/ctx/ctx.py +185 -101
  12. pygpt_net/controller/files/files.py +101 -41
  13. pygpt_net/controller/idx/indexer.py +87 -31
  14. pygpt_net/controller/kernel/kernel.py +13 -2
  15. pygpt_net/controller/mode/mode.py +3 -3
  16. pygpt_net/controller/model/editor.py +70 -15
  17. pygpt_net/controller/model/importer.py +153 -54
  18. pygpt_net/controller/painter/common.py +43 -11
  19. pygpt_net/controller/painter/painter.py +2 -2
  20. pygpt_net/controller/presets/experts.py +68 -15
  21. pygpt_net/controller/presets/presets.py +72 -36
  22. pygpt_net/controller/settings/profile.py +76 -35
  23. pygpt_net/controller/settings/workdir.py +70 -39
  24. pygpt_net/core/assistants/files.py +20 -18
  25. pygpt_net/core/filesystem/actions.py +111 -10
  26. pygpt_net/core/filesystem/filesystem.py +72 -1
  27. pygpt_net/core/filesystem/packer.py +161 -1
  28. pygpt_net/core/idx/idx.py +12 -11
  29. pygpt_net/core/idx/worker.py +13 -1
  30. pygpt_net/core/image/image.py +2 -2
  31. pygpt_net/core/models/models.py +4 -4
  32. pygpt_net/core/profile/profile.py +13 -3
  33. pygpt_net/core/video/video.py +2 -3
  34. pygpt_net/data/config/config.json +3 -3
  35. pygpt_net/data/config/models.json +3 -3
  36. pygpt_net/data/css/style.dark.css +45 -0
  37. pygpt_net/data/css/style.light.css +46 -0
  38. pygpt_net/data/locale/locale.de.ini +5 -1
  39. pygpt_net/data/locale/locale.en.ini +5 -1
  40. pygpt_net/data/locale/locale.es.ini +5 -1
  41. pygpt_net/data/locale/locale.fr.ini +5 -1
  42. pygpt_net/data/locale/locale.it.ini +5 -1
  43. pygpt_net/data/locale/locale.pl.ini +6 -2
  44. pygpt_net/data/locale/locale.uk.ini +5 -1
  45. pygpt_net/data/locale/locale.zh.ini +5 -1
  46. pygpt_net/provider/api/openai/__init__.py +4 -2
  47. pygpt_net/provider/core/config/patch.py +17 -1
  48. pygpt_net/tools/image_viewer/tool.py +17 -0
  49. pygpt_net/tools/text_editor/tool.py +9 -0
  50. pygpt_net/ui/__init__.py +2 -2
  51. pygpt_net/ui/dialog/preset.py +1 -0
  52. pygpt_net/ui/layout/ctx/ctx_list.py +16 -6
  53. pygpt_net/ui/layout/toolbox/image.py +2 -1
  54. pygpt_net/ui/layout/toolbox/indexes.py +2 -0
  55. pygpt_net/ui/layout/toolbox/video.py +5 -1
  56. pygpt_net/ui/main.py +3 -1
  57. pygpt_net/ui/widget/calendar/select.py +3 -3
  58. pygpt_net/ui/widget/draw/painter.py +238 -51
  59. pygpt_net/ui/widget/filesystem/explorer.py +1164 -142
  60. pygpt_net/ui/widget/lists/assistant.py +185 -24
  61. pygpt_net/ui/widget/lists/assistant_store.py +245 -42
  62. pygpt_net/ui/widget/lists/attachment.py +230 -47
  63. pygpt_net/ui/widget/lists/attachment_ctx.py +189 -33
  64. pygpt_net/ui/widget/lists/base_list_combo.py +2 -2
  65. pygpt_net/ui/widget/lists/context.py +1253 -70
  66. pygpt_net/ui/widget/lists/experts.py +110 -8
  67. pygpt_net/ui/widget/lists/model_editor.py +217 -14
  68. pygpt_net/ui/widget/lists/model_importer.py +125 -6
  69. pygpt_net/ui/widget/lists/preset.py +460 -71
  70. pygpt_net/ui/widget/lists/profile.py +149 -27
  71. pygpt_net/ui/widget/lists/uploaded.py +230 -38
  72. pygpt_net/ui/widget/option/combo.py +1211 -33
  73. pygpt_net/ui/widget/option/dictionary.py +35 -7
  74. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/METADATA +22 -57
  75. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/RECORD +78 -78
  76. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/LICENSE +0 -0
  77. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/WHEEL +0 -0
  78. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/entry_points.txt +0 -0
@@ -6,11 +6,11 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.12.25 20:00:00 #
9
+ # Updated Date: 2025.12.27 21:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import re
13
- from typing import Optional, List, Dict
13
+ from typing import Optional, List, Dict, Union
14
14
 
15
15
  from PySide6.QtCore import QTimer
16
16
  from PySide6.QtGui import QTextCursor
@@ -536,25 +536,39 @@ class Presets:
536
536
  """
537
537
  return _FILENAME_SANITIZE_RE.sub('_', name.lower())
538
538
 
539
- def duplicate(self, idx: Optional[int] = None):
539
+ def duplicate(self, idx: Optional[Union[int, list]] = None):
540
540
  """
541
541
  Duplicate preset
542
542
 
543
- :param idx: preset index (row index)
543
+ :param idx: preset ID (or list of IDs)
544
544
  """
545
- if idx is not None:
546
- w = self.window
547
- mode = w.core.config.get('mode')
548
- preset = w.core.presets.get_by_idx(idx, mode)
549
- if preset:
550
- if preset in w.core.presets.items:
551
- new_id = w.core.presets.duplicate(preset)
552
- self.update_list()
553
- self.refresh(no_scroll=True)
554
- idx = w.core.presets.get_idx_by_id(mode, new_id)
555
- self.editor.edit(idx)
556
- self.select(idx) # switch to the new preset
557
- w.update_status(trans('status.preset.duplicated'))
545
+ ids = idx if isinstance(idx, list) else [idx]
546
+ last_id = None
547
+ updated = False
548
+ w = self.window
549
+ mode = w.core.config.get('mode')
550
+ for idx in ids:
551
+ if idx is not None:
552
+ if len(ids) > 1:
553
+ preset = w.core.presets.get_by_id(mode, idx) # by ID, PresetItem
554
+ preset_id = preset.filename if preset else None
555
+ else:
556
+ preset = w.core.presets.get_by_idx(idx, mode) # by index, str
557
+ preset_id = preset
558
+ if preset_id:
559
+ new_id = w.core.presets.duplicate(preset_id)
560
+ last_id = new_id
561
+ updated = True
562
+
563
+ if updated:
564
+ self.update_list()
565
+ self.refresh(no_scroll=True)
566
+ if len(ids) == 1 and idx is not None:
567
+ idx = w.core.presets.get_idx_by_id(mode, last_id)
568
+ self.editor.edit(idx)
569
+ self.select(idx) # switch to the new preset if only one was duplicated
570
+ w.update_status(trans('status.preset.duplicated'))
571
+
558
572
 
559
573
  def enable(self, idx: Optional[int] = None):
560
574
  """
@@ -654,30 +668,49 @@ class Presets:
654
668
 
655
669
  def delete(
656
670
  self,
657
- idx: Optional[int] = None,
671
+ idx: Optional[Union[int, list]] = None,
658
672
  force: bool = False
659
673
  ):
660
674
  """
661
675
  Delete preset
662
676
 
663
- :param idx: preset index (row index)
677
+ :param idx: preset index (row index) or list of indices
664
678
  :param force: force delete without confirmation
665
679
  """
666
- if idx is not None:
667
- w = self.window
668
- mode = w.core.config.get('mode')
669
- preset_id = w.core.presets.get_by_idx(idx, mode)
670
- if preset_id and preset_id in w.core.presets.items:
671
- if not force:
672
- w.ui.dialogs.confirm(
673
- type='preset_delete',
674
- id=idx,
675
- msg=trans('confirm.preset.delete'),
676
- )
677
- return
678
-
679
- # Determine neighbor only if the deleted preset is currently active.
680
- # This keeps API semantics untouched and prevents unexpected selection changes.
680
+ ids = idx if isinstance(idx, list) else [idx]
681
+ w = self.window
682
+ mode = w.core.config.get('mode')
683
+ is_preset = False
684
+ updated = False
685
+ for tmp_id in ids:
686
+ if len(ids) > 1:
687
+ preset = w.core.presets.get_by_id(mode, tmp_id) # by ID, PresetItem
688
+ else:
689
+ preset = w.core.presets.get_by_idx(tmp_id, mode) # by index, str
690
+ if preset:
691
+ is_preset = True
692
+ break
693
+
694
+ if ids and is_preset:
695
+ if not force:
696
+ w.ui.dialogs.confirm(
697
+ type='preset_delete',
698
+ id=idx,
699
+ msg=trans('confirm.preset.delete'),
700
+ )
701
+ return
702
+
703
+ # Determine neighbor only if the deleted preset is currently active.
704
+ # This keeps API semantics untouched and prevents unexpected selection changes.
705
+ for idx in ids:
706
+ if len(ids) > 1:
707
+ preset = w.core.presets.get_by_id(mode, idx) # by ID, PresetItem
708
+ preset_id = preset.filename if preset else None
709
+ else:
710
+ preset = w.core.presets.get_by_idx(idx, mode) # by index, str
711
+ preset_id = preset
712
+ if not preset:
713
+ continue
681
714
  is_current = (preset_id == w.core.config.get('preset'))
682
715
  target_id = None
683
716
  if is_current:
@@ -700,8 +733,11 @@ class Presets:
700
733
  w.core.config.set('preset', None)
701
734
  w.ui.nodes['preset.prompt'].setPlainText("")
702
735
 
703
- self.refresh(no_scroll=True)
704
- w.update_status(trans('status.preset.deleted'))
736
+ updated = True
737
+
738
+ if updated:
739
+ self.refresh(no_scroll=True)
740
+ w.update_status(trans('status.preset.deleted'))
705
741
 
706
742
  def restore(self, force: bool = False):
707
743
  """
@@ -6,12 +6,12 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.09.26 13:00:00 #
9
+ # Updated Date: 2025.12.28 04:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
13
13
  from pathlib import Path
14
- from typing import Optional, Dict, Any
14
+ from typing import Optional, Dict, Any, Union
15
15
 
16
16
  from PySide6.QtCore import Slot, QTimer
17
17
  from PySide6.QtGui import QAction
@@ -173,6 +173,8 @@ class Profile:
173
173
  self.setup()
174
174
  self.initialized = True
175
175
  if not self.dialog or force:
176
+ self.update_menu()
177
+ self.update_list()
176
178
  self.window.ui.dialogs.open(
177
179
  'profile.editor',
178
180
  width=self.width,
@@ -336,18 +338,26 @@ class Profile:
336
338
  uuid = self.get_id_by_idx(idx)
337
339
  self.switch(uuid)
338
340
 
339
- def delete_by_idx(self, idx: int, force: bool = False):
341
+ def delete_by_idx(self, idx: Union[int, list], force: bool = False):
340
342
  """
341
343
  Delete profile by index
342
344
 
343
- :param idx: profile index
345
+ :param idx: profile index or list of indexes
344
346
  :param force: force delete
345
347
  """
346
- uuid = self.get_id_by_idx(idx)
348
+ uuids = []
349
+ ids = idx if isinstance(idx, list) else [idx]
350
+ for i in ids:
351
+ uuid = self.get_id_by_idx(i)
352
+ uuids.append(uuid)
347
353
  current = self.window.core.config.profile.get_current()
348
- if uuid == current:
349
- self.window.ui.dialogs.alert(trans("dialog.profile.alert.delete.current"))
350
- return
354
+ for uuid in list(uuids):
355
+ if uuid == current:
356
+ if len(uuids) == 1:
357
+ self.window.ui.dialogs.alert(trans("dialog.profile.alert.delete.current"))
358
+ return
359
+ else:
360
+ uuids.remove(uuid) # skip current
351
361
  if not force:
352
362
  self.window.ui.dialogs.confirm(
353
363
  type='profile.delete',
@@ -355,35 +365,48 @@ class Profile:
355
365
  msg=trans('confirm.profile.delete'),
356
366
  )
357
367
  return
358
- self.delete(uuid)
368
+ self.delete(uuids)
359
369
 
360
- def delete(self, uuid: str):
370
+ def delete(self, uuid: Union[str, list]):
361
371
  """
362
372
  Delete profile (remove only)
363
373
 
364
- :param uuid: profile ID
374
+ :param uuid: profile ID or list of IDs
365
375
  """
366
376
  profiles = self.get_profiles()
367
- if uuid in profiles:
368
- profile = profiles[uuid]
369
- name = profile['name']
370
- if self.window.core.config.profile.remove(uuid):
371
- self.window.update_status(trans("dialog.profile.status.removed") + ": " + name)
372
- self.update_list()
373
- self.update_menu()
377
+ updated = False
378
+ ids = uuid if isinstance(uuid, list) else [uuid]
379
+ for uuid in ids:
380
+ if uuid in profiles:
381
+ profile = profiles[uuid]
382
+ name = profile['name']
383
+ if self.window.core.config.profile.remove(uuid):
384
+ updated = True
385
+ self.window.update_status(trans("dialog.profile.status.removed") + ": " + name)
386
+ if updated:
387
+ self.update_list()
388
+ self.update_menu()
374
389
 
375
- def delete_all_by_idx(self, idx: int, force: bool = False):
390
+ def delete_all_by_idx(self, idx: Union[int, list], force: bool = False):
376
391
  """
377
392
  Delete profile with files by index
378
393
 
379
- :param idx: profile index
394
+ :param idx: profile index or list of indexes
380
395
  :param force: force delete
381
396
  """
382
- uuid = self.get_id_by_idx(idx)
397
+ uuids = []
398
+ ids = idx if isinstance(idx, list) else [idx]
399
+ for i in ids:
400
+ uuid = self.get_id_by_idx(i)
401
+ uuids.append(uuid)
383
402
  current = self.window.core.config.profile.get_current()
384
- if uuid == current:
385
- self.window.ui.dialogs.alert(trans("dialog.profile.alert.delete.current"))
386
- return
403
+ for uuid in list(uuids):
404
+ if uuid == current:
405
+ if len(uuids) == 1:
406
+ self.window.ui.dialogs.alert(trans("dialog.profile.alert.delete.current"))
407
+ return
408
+ else:
409
+ uuids.remove(uuid) # skip current
387
410
  if not force:
388
411
  self.window.ui.dialogs.confirm(
389
412
  type='profile.delete.all',
@@ -391,7 +414,7 @@ class Profile:
391
414
  msg=trans('confirm.profile.delete_all'),
392
415
  )
393
416
  return
394
- self.delete_all(uuid)
417
+ self.delete_all(uuids)
395
418
 
396
419
  def duplicate_by_idx(self, idx: int):
397
420
  """
@@ -416,13 +439,20 @@ class Profile:
416
439
  dialog.prepare()
417
440
  dialog.show()
418
441
 
419
- def delete_all(self, uuid: str):
442
+ def delete_all(self, uuid: Union[str, list]):
420
443
  """
421
444
  Delete profile with files
422
445
 
423
- :param uuid: profile ID
446
+ :param uuid: profile ID or list of IDs
424
447
  """
425
- self.window.controller.settings.workdir.delete_files(uuid)
448
+ ids = uuid if isinstance(uuid, list) else [uuid]
449
+ batch = False
450
+ if len(ids) > 1:
451
+ batch = True
452
+ if not batch:
453
+ self.window.controller.settings.workdir.delete_files(uuid)
454
+ else:
455
+ self.window.controller.settings.workdir.delete_files(ids, batch=batch)
426
456
 
427
457
  @Slot(str)
428
458
  def after_delete(self, name: str):
@@ -468,22 +498,33 @@ class Profile:
468
498
  if self.window.ui.nodes['dialog.profile.checkbox.switch'].isChecked():
469
499
  self.switch(uuid, force=True)
470
500
 
471
- def reset(self, uuid: str):
501
+ def reset(self, uuid: Union[str, list]):
472
502
  """
473
503
  Reset profile
474
504
 
475
- :param uuid: profile ID
505
+ :param uuid: profile ID or list of IDs
476
506
  """
477
- self.window.controller.settings.workdir.reset(uuid)
507
+ ids = uuid if isinstance(uuid, list) else [uuid]
508
+ batch = False
509
+ if len(ids) > 1:
510
+ batch = True
511
+ if not batch:
512
+ self.window.controller.settings.workdir.reset(uuid)
513
+ else:
514
+ self.window.controller.settings.workdir.reset(ids, batch=batch)
478
515
 
479
- def reset_by_idx(self, idx: int, force: bool = False):
516
+ def reset_by_idx(self, idx: Union[int, list], force: bool = False):
480
517
  """
481
518
  Reset profile by index
482
519
 
483
- :param idx: profile index
520
+ :param idx: profile index or list of indexes
484
521
  :param force: force reset
485
522
  """
486
- uuid = self.get_id_by_idx(idx)
523
+ uuids = []
524
+ ids = idx if isinstance(idx, list) else [idx]
525
+ for i in ids:
526
+ uuid = self.get_id_by_idx(i)
527
+ uuids.append(uuid)
487
528
  if not force:
488
529
  self.window.ui.dialogs.confirm(
489
530
  type='profile.reset',
@@ -491,7 +532,7 @@ class Profile:
491
532
  msg=trans('confirm.profile.reset'),
492
533
  )
493
534
  return
494
- self.reset(uuid)
535
+ self.reset(uuids)
495
536
 
496
537
  def is_include_db(self):
497
538
  """Get include db"""
@@ -6,13 +6,13 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.09.26 13:00:00 #
9
+ # Updated Date: 2025.12.28 04:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
13
13
  import os
14
14
  from pathlib import Path
15
- from typing import Optional
15
+ from typing import Optional, Union
16
16
  from uuid import uuid4
17
17
 
18
18
  from PySide6.QtCore import QObject, QRunnable, Signal, Slot
@@ -50,9 +50,10 @@ class WorkdirWorker(QRunnable):
50
50
  force: bool = False,
51
51
  current: Optional[str] = None,
52
52
  profile_name: Optional[str] = None,
53
- profile_uuid: Optional[str] = None,
53
+ profile_uuid: Optional[Union[str, list]] = None,
54
54
  profile_new_name: Optional[str] = None,
55
55
  profile_new_path: Optional[str] = None,
56
+ batch: bool = False,
56
57
  ):
57
58
  super().__init__()
58
59
  self.window = window
@@ -65,6 +66,7 @@ class WorkdirWorker(QRunnable):
65
66
  self.profile_new_name = profile_new_name
66
67
  self.profile_new_path = profile_new_path
67
68
  self.signals = WorkerSignals()
69
+ self.batch = batch
68
70
 
69
71
  @Slot()
70
72
  def run(self):
@@ -104,22 +106,34 @@ class WorkdirWorker(QRunnable):
104
106
  profiles = self.window.controller.settings.profile.get_profiles()
105
107
  remove_datadir = True
106
108
  remove_db = True
107
- if uuid in profiles:
108
- profile = profiles[uuid]
109
- name = profile['name']
110
- path = profile['workdir'].replace("%HOME%", str(Path.home()))
111
- # remove profile
112
- if self.window.core.config.profile.remove(uuid):
113
- if not os.path.exists(path) or not os.path.isdir(path):
114
- self.signals.alert.emit(trans("dialog.profile.alert.path.not_exists"))
115
- return
116
- print(f"Clearing workdir: {path}")
117
- self.window.core.filesystem.clear_workdir(
118
- path,
119
- remove_db=remove_db,
120
- remove_datadir=remove_datadir,
121
- )
122
- self.signals.deleted.emit(name)
109
+ uuids = uuid if isinstance(uuid, list) else [uuid]
110
+ for uuid in uuids:
111
+ if uuid in profiles:
112
+ profile = profiles[uuid]
113
+ name = profile['name']
114
+ path = profile['workdir'].replace("%HOME%", str(Path.home()))
115
+ # remove profile
116
+ if self.window.core.config.profile.remove(uuid):
117
+ if not os.path.exists(path) or not os.path.isdir(path):
118
+ if not self.batch:
119
+ self.signals.deleted.emit(name)
120
+ self.signals.alert.emit(trans("dialog.profile.alert.path.not_exists"))
121
+ return
122
+ else:
123
+ print("Directory not exists, ignoring: ", path)
124
+ self.signals.updateGlobalStatus.emit(f"Directory not exists: {path}")
125
+ self.signals.deleted.emit(name)
126
+ return # nothing to delete
127
+ print(f"Clearing workdir: {path}")
128
+ try:
129
+ self.window.core.filesystem.clear_workdir(
130
+ path,
131
+ remove_db=remove_db,
132
+ remove_datadir=remove_datadir,
133
+ )
134
+ except Exception as e:
135
+ print("Error deleting profile files: ", e)
136
+ self.signals.deleted.emit(name)
123
137
 
124
138
  def worker_duplicate(self):
125
139
  """Duplicate profile"""
@@ -168,22 +182,30 @@ class WorkdirWorker(QRunnable):
168
182
  current = self.window.core.config.profile.get_current()
169
183
  remove_datadir = False
170
184
  remove_db = True
171
- if uuid in profiles:
172
- profile = profiles[uuid]
173
- path = profile['workdir'].replace("%HOME%", str(Path.home()))
174
- if not os.path.exists(path) or not os.path.isdir(path):
175
- self.signals.alert.emit(f"Directory not exists: {path}")
176
- return
177
- print("Clearing workdir: ", path)
178
- self.window.core.db.close()
179
- self.window.core.filesystem.clear_workdir(
180
- path,
181
- remove_db=remove_db,
182
- remove_datadir=remove_datadir,
183
- )
184
- if uuid == current:
185
- self.signals.switch.emit(uuid) # switch to profile
186
- self.signals.updateGlobalStatus.emit(f"Profile cleared: {profile['name']}")
185
+ uuids = uuid if isinstance(uuid, list) else [uuid]
186
+ for uuid in uuids:
187
+ if uuid in profiles:
188
+ profile = profiles[uuid]
189
+ path = profile['workdir'].replace("%HOME%", str(Path.home()))
190
+ if not os.path.exists(path) or not os.path.isdir(path):
191
+ if not self.batch:
192
+ self.signals.alert.emit(f"Directory not exists: {path}")
193
+ return
194
+ else:
195
+ print("Directory not exists, ignoring: ", path)
196
+ self.signals.updateGlobalStatus.emit(f"Directory not exists: {path}")
197
+ return
198
+ print("Clearing workdir: ", path)
199
+ if uuid == current:
200
+ self.window.core.db.close()
201
+ self.window.core.filesystem.clear_workdir(
202
+ path,
203
+ remove_db=remove_db,
204
+ remove_datadir=remove_datadir,
205
+ )
206
+ if uuid == current and not self.batch:
207
+ self.signals.switch.emit(uuid) # switch to profile
208
+ self.signals.updateGlobalStatus.emit(f"Profile cleared: {profile['name']}")
187
209
 
188
210
  def worker_migrate(self):
189
211
  """Migrate working directory"""
@@ -471,9 +493,10 @@ class Workdir:
471
493
  force: bool = False,
472
494
  current: str = None,
473
495
  profile_name: Optional[str] = None,
474
- profile_uuid: Optional[str] = None,
496
+ profile_uuid: Optional[Union[str, list]] = None,
475
497
  profile_new_name: Optional[str] = None,
476
498
  profile_new_path: Optional[str] = None,
499
+ batch: bool = False,
477
500
  ):
478
501
  """
479
502
  Run action in thread
@@ -483,9 +506,10 @@ class Workdir:
483
506
  :param force: force action (confirm)
484
507
  :param current: current working directory (for restore action)
485
508
  :param profile_name: profile name (optional, for after update callback)
486
- :param profile_uuid: profile UUID (optional, for delete files action)
509
+ :param profile_uuid: profile UUID (optional, for delete files action) or list of UUIDs
487
510
  :param profile_new_name: new profile name (optional, for duplicate action)
488
511
  :param profile_new_path: new profile path (optional, for duplicate action)
512
+ :param batch: if True, run in batch mode (no UI updates)
489
513
  """
490
514
  worker = WorkdirWorker(
491
515
  window=self.window,
@@ -497,6 +521,7 @@ class Workdir:
497
521
  profile_uuid=profile_uuid,
498
522
  profile_new_name=profile_new_name,
499
523
  profile_new_path=profile_new_path,
524
+ batch=batch,
500
525
  )
501
526
 
502
527
  # connect signals
@@ -569,16 +594,19 @@ class Workdir:
569
594
 
570
595
  def delete_files(
571
596
  self,
572
- profile_uuid: str
597
+ profile_uuid: Union[str, list],
598
+ batch: bool = False
573
599
  ):
574
600
  """
575
601
  Delete files and directories associated with the profile
576
602
 
577
603
  :param profile_uuid: profile UUID
604
+ :param batch: if True, run in batch mode (no UI updates)
578
605
  """
579
606
  self.run_action(
580
607
  action="delete",
581
608
  profile_uuid=profile_uuid,
609
+ batch=batch,
582
610
  )
583
611
 
584
612
  def duplicate(
@@ -603,14 +631,17 @@ class Workdir:
603
631
 
604
632
  def reset(
605
633
  self,
606
- profile_uuid: str
634
+ profile_uuid: Union[str, list],
635
+ batch: bool = False
607
636
  ):
608
637
  """
609
638
  Reset profile
610
639
 
611
640
  :param profile_uuid: profile UUID to reset
641
+ :param batch: if True, run in batch mode (no UI updates)
612
642
  """
613
643
  self.run_action(
614
644
  action="reset",
615
645
  profile_uuid=profile_uuid,
646
+ batch=batch,
616
647
  )
@@ -6,10 +6,10 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2024.12.14 08:00:00 #
9
+ # Updated Date: 2025.12.27 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from typing import Optional, List, Dict
12
+ from typing import Optional, List, Dict, Union
13
13
 
14
14
  from packaging.version import Version
15
15
 
@@ -231,30 +231,32 @@ class Files:
231
231
  """
232
232
  return self.provider.get_all_by_file_id(file_id)
233
233
 
234
- def delete(self, file: AssistantFileItem) -> bool:
234
+ def delete(self, file: Union[AssistantFileItem, list]) -> bool:
235
235
  """
236
236
  Delete file and remove from vector stores if exists
237
237
 
238
238
  :param file: file item
239
239
  :return: True if file was deleted
240
240
  """
241
- file_id = file.file_id
242
- items = self.get_all_by_file_id(file_id) # get store_ids
243
- for id in items:
244
- store_id = items[id].store_id
245
- if store_id is None or store_id == "":
246
- continue # skip if no store_id
241
+ files = file if isinstance(file, list) else [file]
242
+ for file in files:
243
+ file_id = file.file_id
244
+ items = self.get_all_by_file_id(file_id) # get store_ids
245
+ for id in items:
246
+ store_id = items[id].store_id
247
+ if store_id is None or store_id == "":
248
+ continue # skip if no store_id
249
+ try:
250
+ self.window.core.api.openai.store.delete_store_file(store_id, file_id) # remove from vector store
251
+ except Exception as e:
252
+ self.window.core.debug.log("Failed to delete file from vector store: " + str(e))
253
+ self.provider.delete_by_id(file.record_id) # delete file in DB
247
254
  try:
248
- self.window.core.api.openai.store.delete_store_file(store_id, file_id) # remove from vector store
255
+ self.window.core.api.openai.store.delete_file(file.file_id) # delete file in API
249
256
  except Exception as e:
250
- self.window.core.debug.log("Failed to delete file from vector store: " + str(e))
251
- self.provider.delete_by_id(file.record_id) # delete file in DB
252
- try:
253
- self.window.core.api.openai.store.delete_file(file.file_id) # delete file in API
254
- except Exception as e:
255
- self.window.core.debug.log("Failed to delete remote file: " + str(e))
256
- if file.record_id in self.items:
257
- del self.items[file.record_id]
257
+ self.window.core.debug.log("Failed to delete remote file: " + str(e))
258
+ if file.record_id in self.items:
259
+ del self.items[file.record_id]
258
260
  return True
259
261
 
260
262
  def delete_by_file_id(self, file_id: str) -> bool: