pygpt-net 2.6.59__py3-none-any.whl → 2.6.61__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 (91) hide show
  1. pygpt_net/CHANGELOG.txt +11 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +9 -5
  4. pygpt_net/controller/__init__.py +1 -0
  5. pygpt_net/controller/chat/common.py +115 -6
  6. pygpt_net/controller/chat/input.py +4 -1
  7. pygpt_net/controller/presets/editor.py +442 -39
  8. pygpt_net/controller/presets/presets.py +121 -6
  9. pygpt_net/controller/settings/editor.py +0 -15
  10. pygpt_net/controller/theme/markdown.py +2 -5
  11. pygpt_net/controller/ui/ui.py +4 -7
  12. pygpt_net/core/agents/custom/__init__.py +281 -0
  13. pygpt_net/core/agents/custom/debug.py +64 -0
  14. pygpt_net/core/agents/custom/factory.py +109 -0
  15. pygpt_net/core/agents/custom/graph.py +71 -0
  16. pygpt_net/core/agents/custom/llama_index/__init__.py +10 -0
  17. pygpt_net/core/agents/custom/llama_index/factory.py +100 -0
  18. pygpt_net/core/agents/custom/llama_index/router_streamer.py +106 -0
  19. pygpt_net/core/agents/custom/llama_index/runner.py +562 -0
  20. pygpt_net/core/agents/custom/llama_index/stream.py +56 -0
  21. pygpt_net/core/agents/custom/llama_index/utils.py +253 -0
  22. pygpt_net/core/agents/custom/logging.py +50 -0
  23. pygpt_net/core/agents/custom/memory.py +51 -0
  24. pygpt_net/core/agents/custom/router.py +155 -0
  25. pygpt_net/core/agents/custom/router_streamer.py +187 -0
  26. pygpt_net/core/agents/custom/runner.py +455 -0
  27. pygpt_net/core/agents/custom/schema.py +127 -0
  28. pygpt_net/core/agents/custom/utils.py +193 -0
  29. pygpt_net/core/agents/provider.py +72 -7
  30. pygpt_net/core/agents/runner.py +7 -4
  31. pygpt_net/core/agents/runners/helpers.py +1 -1
  32. pygpt_net/core/agents/runners/llama_workflow.py +3 -0
  33. pygpt_net/core/agents/runners/openai_workflow.py +8 -1
  34. pygpt_net/core/db/viewer.py +11 -5
  35. pygpt_net/{ui/widget/builder → core/node_editor}/__init__.py +2 -2
  36. pygpt_net/core/{builder → node_editor}/graph.py +28 -226
  37. pygpt_net/core/node_editor/models.py +118 -0
  38. pygpt_net/core/node_editor/types.py +78 -0
  39. pygpt_net/core/node_editor/utils.py +17 -0
  40. pygpt_net/core/presets/presets.py +216 -29
  41. pygpt_net/core/render/markdown/parser.py +0 -2
  42. pygpt_net/core/render/web/renderer.py +10 -8
  43. pygpt_net/data/config/config.json +5 -6
  44. pygpt_net/data/config/models.json +3 -3
  45. pygpt_net/data/config/settings.json +2 -38
  46. pygpt_net/data/locale/locale.de.ini +64 -1
  47. pygpt_net/data/locale/locale.en.ini +63 -4
  48. pygpt_net/data/locale/locale.es.ini +64 -1
  49. pygpt_net/data/locale/locale.fr.ini +64 -1
  50. pygpt_net/data/locale/locale.it.ini +64 -1
  51. pygpt_net/data/locale/locale.pl.ini +65 -2
  52. pygpt_net/data/locale/locale.uk.ini +64 -1
  53. pygpt_net/data/locale/locale.zh.ini +64 -1
  54. pygpt_net/data/locale/plugin.cmd_system.en.ini +62 -66
  55. pygpt_net/item/agent.py +5 -1
  56. pygpt_net/item/preset.py +19 -1
  57. pygpt_net/provider/agents/base.py +33 -2
  58. pygpt_net/provider/agents/llama_index/flow_from_schema.py +92 -0
  59. pygpt_net/provider/agents/openai/flow_from_schema.py +96 -0
  60. pygpt_net/provider/core/agent/json_file.py +11 -5
  61. pygpt_net/provider/core/config/patch.py +10 -1
  62. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +0 -6
  63. pygpt_net/tools/agent_builder/tool.py +233 -52
  64. pygpt_net/tools/agent_builder/ui/dialogs.py +172 -28
  65. pygpt_net/tools/agent_builder/ui/list.py +37 -10
  66. pygpt_net/ui/__init__.py +2 -4
  67. pygpt_net/ui/dialog/about.py +58 -38
  68. pygpt_net/ui/dialog/db.py +142 -3
  69. pygpt_net/ui/dialog/preset.py +62 -8
  70. pygpt_net/ui/layout/toolbox/presets.py +52 -16
  71. pygpt_net/ui/main.py +1 -1
  72. pygpt_net/ui/widget/dialog/db.py +0 -0
  73. pygpt_net/ui/widget/lists/preset.py +644 -60
  74. pygpt_net/{core/builder → ui/widget/node_editor}/__init__.py +2 -2
  75. pygpt_net/ui/widget/node_editor/command.py +373 -0
  76. pygpt_net/ui/widget/node_editor/config.py +157 -0
  77. pygpt_net/ui/widget/node_editor/editor.py +2070 -0
  78. pygpt_net/ui/widget/node_editor/item.py +493 -0
  79. pygpt_net/ui/widget/node_editor/node.py +1460 -0
  80. pygpt_net/ui/widget/node_editor/utils.py +17 -0
  81. pygpt_net/ui/widget/node_editor/view.py +364 -0
  82. pygpt_net/ui/widget/tabs/output.py +1 -1
  83. pygpt_net/ui/widget/textarea/input.py +2 -2
  84. pygpt_net/utils.py +114 -2
  85. {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/METADATA +80 -93
  86. {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/RECORD +88 -61
  87. pygpt_net/core/agents/custom.py +0 -150
  88. pygpt_net/ui/widget/builder/editor.py +0 -2001
  89. {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/LICENSE +0 -0
  90. {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/WHEEL +0 -0
  91. {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/entry_points.txt +0 -0
@@ -6,7 +6,7 @@
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.08.28 09:00:00 #
9
+ # Updated Date: 2025.09.24 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import datetime
@@ -206,6 +206,7 @@ class Editor:
206
206
  }
207
207
  self.id = "preset"
208
208
  self.current = None
209
+ self.current_id = None
209
210
 
210
211
  def get_options(self) -> Dict[str, Dict[str, Any]]:
211
212
  """
@@ -246,12 +247,7 @@ class Editor:
246
247
  """Setup preset editor"""
247
248
  # update after agents register
248
249
  self.append_extra_config()
249
- self.window.ui.config[self.id]['agent_provider'].set_keys(
250
- self.window.controller.config.placeholder.apply_by_id('agent_provider_llama')
251
- )
252
- self.window.ui.config[self.id]['agent_provider_openai'].set_keys(
253
- self.window.controller.config.placeholder.apply_by_id('agent_provider_openai')
254
- )
250
+ self.update_providers_list()
255
251
 
256
252
  # add hooks for config update in real-time
257
253
  self.window.ui.add_hook("update.preset.prompt", self.hook_update)
@@ -268,63 +264,74 @@ class Editor:
268
264
  tabs.setTabVisible(0, True) # show base prompt
269
265
  for opt_id in self.tab_options_idx: # hide all tabs
270
266
  for tab_idx in self.tab_options_idx[opt_id]:
271
- if tabs.count() >= tab_idx:
267
+ if tabs.count() > tab_idx:
272
268
  tabs.setTabVisible(tab_idx, False)
273
269
  return
274
270
  else:
275
271
  for opt_id in self.tab_options_idx: # hide all tabs
276
272
  for tab_idx in self.tab_options_idx[opt_id]:
277
- if tabs.count() >= tab_idx:
273
+ if tabs.count() > tab_idx:
278
274
  tabs.setTabVisible(tab_idx, False)
279
275
 
280
276
  self.toggle_extra_options_by_provider()
281
277
 
282
278
  def toggle_extra_options_by_provider(self):
283
279
  """Toggle extra options in preset editor by provider"""
280
+ tabs = self.window.ui.tabs['preset.editor.extra']
281
+
284
282
  if not self.tab_options_idx:
285
- self.window.ui.tabs['preset.editor.extra'].setTabVisible(0, True) # show base prompt
283
+ tabs.setTabVisible(0, True) # base prompt
286
284
  return
287
285
 
288
286
  mode = self.window.core.config.get('mode')
289
- tabs = self.window.ui.tabs['preset.editor.extra']
290
287
  key_agent = ""
291
288
 
292
289
  if mode in [MODE_AGENT_OPENAI, MODE_AGENT_LLAMA]:
293
- # get current provider
294
- if mode == MODE_AGENT_LLAMA:
295
- key_agent = "agent_provider"
296
- elif mode == MODE_AGENT_OPENAI:
297
- key_agent = "agent_provider_openai"
290
+ key_agent = "agent_provider_openai" if mode == MODE_AGENT_OPENAI else "agent_provider"
298
291
 
292
+ # 1) try from UI
299
293
  current_provider = self.window.controller.config.get_value(
300
294
  parent_id=self.id,
301
295
  key=key_agent,
302
296
  option=self.options[key_agent],
303
297
  )
304
- if current_provider is None or current_provider == "":
305
- tabs.setTabVisible(0, True) # show base prompt
298
+
299
+ # 2) fallback to current preset
300
+ if not current_provider or current_provider == "_":
301
+ preset = self.window.core.presets.get_by_uuid(self.current)
302
+ if preset:
303
+ current_provider = getattr(preset, key_agent, None)
304
+
305
+ # 3) if still not set -> show base prompt
306
+ if not current_provider or current_provider == "_":
307
+ tabs.setTabVisible(0, True)
306
308
  return
307
309
 
308
- # show all tabs for current provider
310
+ # first hide all tabs
309
311
  for opt_id in self.tab_options_idx:
310
- tabs.setTabVisible(0, False)
311
- if opt_id != current_provider:
312
- for tab_idx in self.tab_options_idx[opt_id]:
313
- if tabs.count() >= tab_idx:
314
- tabs.setTabVisible(tab_idx, False)
315
- else:
316
- for tab_idx in self.tab_options_idx[opt_id]:
317
- if tabs.count() >= tab_idx:
318
- tabs.setTabVisible(tab_idx, True)
312
+ for tab_idx in self.tab_options_idx[opt_id]:
313
+ if tabs.count() > tab_idx:
314
+ tabs.setTabVisible(tab_idx, False)
315
+
316
+ # hide base prompt
317
+ tabs.setTabVisible(0, False)
319
318
 
320
- # show base prompt if no custom options in current agent
321
- agent = self.window.core.agents.provider.get(current_provider)
319
+ # show tabs for current provider
320
+ for tab_idx in self.tab_options_idx.get(current_provider, []):
321
+ if tabs.count() > tab_idx:
322
+ tabs.setTabVisible(tab_idx, True)
323
+
324
+ # if not found, show base prompt
325
+ agent = self.window.core.agents.provider.get(current_provider, mode)
322
326
  if not agent:
323
327
  tabs.setTabVisible(0, True)
324
328
  return
325
329
  option_tabs = agent.get_options()
326
- if not option_tabs or len(option_tabs) == 0:
330
+ if not option_tabs:
327
331
  tabs.setTabVisible(0, True)
332
+ else:
333
+ # not agent mode -> show base prompt
334
+ tabs.setTabVisible(0, True)
328
335
 
329
336
  def load_extra_options(self, preset: PresetItem):
330
337
  """
@@ -345,7 +352,7 @@ class Editor:
345
352
  return
346
353
 
347
354
  # update options in UI
348
- agent = self.window.core.agents.provider.get(id)
355
+ agent = self.window.core.agents.provider.get(id, mode)
349
356
  if not agent:
350
357
  return
351
358
  if not preset.extra or id not in preset.extra:
@@ -398,7 +405,7 @@ class Editor:
398
405
 
399
406
  # load defaults for all tabs
400
407
  for id in self.tab_options_idx:
401
- agent = self.window.core.agents.provider.get(id)
408
+ agent = self.window.core.agents.provider.get(id, mode)
402
409
  if not agent:
403
410
  continue
404
411
  option_tabs = agent.get_options()
@@ -483,7 +490,6 @@ class Editor:
483
490
  exclude_ids = [
484
491
  "__prompt__",
485
492
  ]
486
- id = None
487
493
  if mode == MODE_AGENT_OPENAI:
488
494
  id = preset.agent_provider_openai
489
495
  elif mode == MODE_AGENT_LLAMA:
@@ -493,7 +499,7 @@ class Editor:
493
499
 
494
500
  get_value = self.window.controller.config.get_value
495
501
  options = {}
496
- agent = self.window.core.agents.provider.get(id)
502
+ agent = self.window.core.agents.provider.get(id, mode)
497
503
  if not agent:
498
504
  return options
499
505
  option_tabs = agent.get_options()
@@ -574,6 +580,217 @@ class Editor:
574
580
 
575
581
  self.built = True
576
582
 
583
+ def _ensure_agent_tab_properties(self):
584
+ """
585
+ Ensure every existing extra tab has the 'agent_id' property set,
586
+ based on current self.tab_options_idx mapping.
587
+ This makes it possible to rebuild the mapping after insertions/removals.
588
+ """
589
+ tabs = self.window.ui.tabs['preset.editor.extra']
590
+ if not tabs:
591
+ return
592
+ for a_id, indices in self.tab_options_idx.items():
593
+ for idx in indices:
594
+ if 0 < idx < tabs.count():
595
+ w = tabs.widget(idx)
596
+ if w is not None and not w.property("agent_id"):
597
+ w.setProperty("agent_id", a_id)
598
+
599
+ def _rebuild_tab_index_mapping(self):
600
+ """
601
+ Rebuild self.tab_options_idx by scanning actual tabs and grouping by 'agent_id' property.
602
+ Base prompt tab (index 0) is ignored.
603
+ """
604
+ tabs = self.window.ui.tabs['preset.editor.extra']
605
+ new_map = {}
606
+ if not tabs:
607
+ self.tab_options_idx = new_map
608
+ return
609
+
610
+ for i in range(1, tabs.count()):
611
+ w = tabs.widget(i)
612
+ if w is None:
613
+ continue
614
+ a_id = w.property("agent_id")
615
+ if not a_id:
616
+ # If a tab has no agent tag, we skip it; it's not an "agent extra" tab.
617
+ continue
618
+ if a_id not in new_map:
619
+ new_map[a_id] = []
620
+ new_map[a_id].append(i)
621
+
622
+ self.tab_options_idx = new_map
623
+
624
+ def update_custom_agent_options(self, agent_id: str):
625
+ """
626
+ Rebuild extra option tabs for a given agent_id at runtime, keeping indices consistent.
627
+
628
+ What it does:
629
+ - Removes all existing tabs for this agent (and their UI config groups).
630
+ - If agent exists, creates fresh tabs from agent.get_options().
631
+ - Applies saved values from current preset.extra[agent_id] when available.
632
+ - Otherwise applies defaults defined in option schema.
633
+ - If agent does not exist, simply clears tabs and config for this agent.
634
+ - Recomputes self.tab_options_idx so it always matches the current QTabWidget.
635
+ - Finally, re-runs visibility logic so only relevant tabs are shown.
636
+
637
+ Notes:
638
+ - Base prompt tab is index 0 and remains untouched.
639
+ - This method must be called in the UI thread (Qt).
640
+ """
641
+ tabs = self.window.ui.tabs['preset.editor.extra']
642
+ if not tabs:
643
+ return
644
+
645
+ # Make sure existing tabs have proper metadata to allow safe remapping later.
646
+ self._ensure_agent_tab_properties()
647
+
648
+ mode = self.window.core.config.get('mode')
649
+ exclude_ids = ["__prompt__"]
650
+
651
+ # Old indices for this agent (if any), sorted to compute insertion position and to remove safely.
652
+ old_indices = sorted(self.tab_options_idx.get(agent_id, []))
653
+ # Prefer to keep the first old index as insertion anchor to preserve overall ordering.
654
+ insertion_index = old_indices[0] if old_indices else tabs.count()
655
+
656
+ # 1) Remove old tabs for this agent (descending order to avoid index shifts).
657
+ for idx in sorted(old_indices, reverse=True):
658
+ if 0 < idx < tabs.count():
659
+ w = tabs.widget(idx)
660
+ tabs.removeTab(idx)
661
+ if w is not None:
662
+ w.deleteLater()
663
+
664
+ # Remove old UI config groups for this agent (they will be rebuilt).
665
+ # We assume self.window.ui.config is a dict-like structure holding per-parent groups.
666
+ cfg = self.window.ui.config
667
+ for key in list(cfg.keys()):
668
+ if isinstance(key, str) and key.startswith(f"agent.{agent_id}."):
669
+ try:
670
+ del cfg[key]
671
+ except Exception:
672
+ pass
673
+
674
+ # Clear mapping entry for this agent; we will rebuild mapping from scratch later.
675
+ if agent_id in self.tab_options_idx:
676
+ del self.tab_options_idx[agent_id]
677
+
678
+ # 2) Fetch fresh options from provider.
679
+ agent = self.window.core.agents.provider.get(agent_id, mode)
680
+ if not agent:
681
+ # Agent no longer exists -> just rebuild mapping for remaining tabs and update visibility.
682
+ self._rebuild_tab_index_mapping()
683
+ self.toggle_extra_options_by_provider()
684
+ return
685
+
686
+ option_tabs = agent.get_options() or {}
687
+
688
+ # If agent has no custom option tabs -> nothing to add (base prompt will be used).
689
+ # Still need to rebuild mapping and visibility.
690
+ if len(option_tabs) == 0:
691
+ self._rebuild_tab_index_mapping()
692
+ self.toggle_extra_options_by_provider()
693
+ return
694
+
695
+ build_option_widgets = self.window.ui.dialogs.preset.build_option_widgets
696
+ apply_value = self.window.controller.config.apply_value
697
+
698
+ # Try to load saved values for current preset (if this agent is used in it).
699
+ preset = self.window.core.presets.get_by_uuid(self.current)
700
+ saved_data = None
701
+ if preset and preset.extra and agent_id in preset.extra:
702
+ # Expected structure: preset.extra[agent_id][option_tab_id][key] = value
703
+ saved_data = preset.extra.get(agent_id, None)
704
+
705
+ # Figure out if this agent is the currently selected provider for the active mode.
706
+ # If yes, we will prefer saved values from preset.extra; otherwise we apply defaults.
707
+ selected_provider_id = None
708
+ if mode in [MODE_AGENT_OPENAI, MODE_AGENT_LLAMA]:
709
+ key_agent = "agent_provider_openai" if mode == MODE_AGENT_OPENAI else "agent_provider"
710
+ selected_provider_id = self.window.controller.config.get_value(
711
+ parent_id=self.id,
712
+ key=key_agent,
713
+ option=self.options[key_agent],
714
+ )
715
+
716
+ # 3) Create new tabs from fresh schema and insert them at the computed anchor.
717
+ new_indices = []
718
+ for option_tab_id, option_desc in option_tabs.items():
719
+ if option_tab_id in exclude_ids:
720
+ continue
721
+
722
+ title = option_desc.get('label', '')
723
+ config_id = f"agent.{agent_id}.{option_tab_id}"
724
+ schema_options = option_desc.get('options', {}) or {}
725
+
726
+ # Build new option widgets into UI config under config_id.
727
+ widgets, options_layouts = build_option_widgets(config_id, schema_options)
728
+
729
+ # Create layouts similar to initial build (checkboxes at bottom row).
730
+ layout = QVBoxLayout()
731
+ layout.setContentsMargins(0, 10, 0, 10)
732
+
733
+ checkbox_layout = QHBoxLayout()
734
+ for key, opt_layout in options_layouts.items():
735
+ opt_schema = schema_options.get(key, {})
736
+ if opt_schema.get('type') == 'bool':
737
+ checkbox_layout.addLayout(opt_layout)
738
+ else:
739
+ layout.addLayout(opt_layout)
740
+ layout.addStretch(1)
741
+ layout.addLayout(checkbox_layout)
742
+
743
+ # Assemble tab widget and tag it with metadata.
744
+ tab_widget = QWidget()
745
+ tab_widget.setLayout(layout)
746
+ tab_widget.setProperty('agent_id', agent_id)
747
+ tab_widget.setProperty('option_tab_id', option_tab_id)
748
+
749
+ # Insert at a stable anchor to preserve general ordering between agents.
750
+ insertion_index = min(insertion_index, tabs.count())
751
+ tabs.insertTab(insertion_index, tab_widget, title)
752
+ new_indices.append(insertion_index)
753
+ insertion_index += 1
754
+
755
+ # Apply values:
756
+ # - If this agent is currently selected for the active mode and we have saved_data,
757
+ # prefer saved values from preset.extra.
758
+ # - Otherwise apply defaults from schema (when provided).
759
+ group_cfg = self.window.ui.config.get(config_id, {})
760
+ if selected_provider_id == agent_id and saved_data and option_tab_id in saved_data:
761
+ data_dict = saved_data[option_tab_id] or {}
762
+ for key, opt_schema in schema_options.items():
763
+ if key in data_dict:
764
+ apply_value(
765
+ parent_id=config_id,
766
+ key=key,
767
+ option=opt_schema,
768
+ value=data_dict[key],
769
+ )
770
+ elif 'default' in opt_schema:
771
+ apply_value(
772
+ parent_id=config_id,
773
+ key=key,
774
+ option=opt_schema,
775
+ value=opt_schema.get('default'),
776
+ )
777
+ else:
778
+ # Apply defaults only (do not overwrite elsewhere).
779
+ for key, opt_schema in schema_options.items():
780
+ if 'default' in opt_schema:
781
+ apply_value(
782
+ parent_id=config_id,
783
+ key=key,
784
+ option=opt_schema,
785
+ value=opt_schema.get('default'),
786
+ )
787
+
788
+ # 4) Recompute mapping fully based on actual tabs and their 'agent_id' properties.
789
+ self._rebuild_tab_index_mapping()
790
+
791
+ # 5) Update visibility according to current mode/provider.
792
+ self.toggle_extra_options_by_provider()
793
+
577
794
  def append_default_prompt(self):
578
795
  """Append default prompt to the preset editor"""
579
796
  mode = self.window.core.config.get('mode')
@@ -616,6 +833,15 @@ class Editor:
616
833
  value=default_prompt,
617
834
  )
618
835
 
836
+ def update_providers_list(self):
837
+ """Update providers list in the preset editor"""
838
+ self.window.ui.config[self.id]['agent_provider'].set_keys(
839
+ self.window.controller.config.placeholder.apply_by_id('agent_provider_llama')
840
+ )
841
+ self.window.ui.config[self.id]['agent_provider_openai'].set_keys(
842
+ self.window.controller.config.placeholder.apply_by_id('agent_provider_openai')
843
+ )
844
+
619
845
  def hook_update(
620
846
  self,
621
847
  key: str,
@@ -660,6 +886,17 @@ class Editor:
660
886
  self.init(preset)
661
887
  self.window.ui.dialogs.open_editor('editor.preset.presets', idx, width=800)
662
888
 
889
+ def reload_all(self, all: bool = False):
890
+ """
891
+ Reload all data in the preset editor
892
+
893
+ :param all: reload all custom agent options
894
+ """
895
+ self.update_providers_list()
896
+ if all:
897
+ self.reload_all_custom_agent_options()
898
+ if self.opened:
899
+ self.init(self.current_id)
663
900
  def init(self, id: Optional[str] = None):
664
901
  """
665
902
  Initialize preset editor
@@ -667,6 +904,9 @@ class Editor:
667
904
  :param id: preset id (filename)
668
905
  """
669
906
  self.opened = True
907
+ self.current_id = id
908
+ self.reload_all()
909
+
670
910
  data = PresetItem()
671
911
  data.name = ""
672
912
  data.filename = ""
@@ -683,6 +923,10 @@ class Editor:
683
923
  else:
684
924
  self.load_extra_defaults()
685
925
 
926
+ mode = self.window.core.config.get("mode")
927
+ if mode in [MODE_AGENT_OPENAI, MODE_AGENT_LLAMA]:
928
+ self.reload_all_custom_agent_options()
929
+
686
930
  if data.name is None:
687
931
  data.name = ""
688
932
  if data.ai_name is None:
@@ -696,7 +940,6 @@ class Editor:
696
940
 
697
941
  # set current mode at start
698
942
  if id is None:
699
- mode = self.window.core.config.get("mode")
700
943
  if mode == MODE_CHAT:
701
944
  data.chat = True
702
945
  elif mode == MODE_COMPLETION:
@@ -865,11 +1108,17 @@ class Editor:
865
1108
  # if agent, assign experts and select only agent mode
866
1109
  curr_mode = self.window.core.config.get('mode')
867
1110
  if curr_mode == MODE_AGENT:
868
- self.window.core.presets.items[preset_id].mode = [MODE_AGENT]
1111
+ itm = self.window.core.presets.items[preset_id]
1112
+ itm.reset_modes()
1113
+ itm.agent = True
869
1114
  elif curr_mode == MODE_AGENT_LLAMA:
870
- self.window.core.presets.items[preset_id].mode = [MODE_AGENT_LLAMA]
1115
+ itm = self.window.core.presets.items[preset_id]
1116
+ itm.reset_modes()
1117
+ itm.agent_llama = True
871
1118
  elif curr_mode == MODE_AGENT_OPENAI:
872
- self.window.core.presets.items[preset_id].mode = [MODE_AGENT_OPENAI]
1119
+ itm = self.window.core.presets.items[preset_id]
1120
+ itm.reset_modes()
1121
+ itm.agent_openai = True
873
1122
 
874
1123
  # apply changes to current active preset
875
1124
  current = self.window.core.config.get('preset')
@@ -1126,3 +1375,157 @@ class Editor:
1126
1375
  else:
1127
1376
  tabs.setTabEnabled(idx, False)
1128
1377
  tabs.setTabVisible(idx, False)
1378
+
1379
+ def reload_all_custom_agent_options(self, purge_missing_from_preset: bool = False):
1380
+ """
1381
+ Full, safe rebuild of all agent-specific option tabs.
1382
+
1383
+ When to use:
1384
+ - Call this at editor opening or whenever multiple agent configs may have changed at once.
1385
+
1386
+ What it does:
1387
+ - Updates provider combos (in case providers changed).
1388
+ - Removes ALL existing "agent extra" tabs (keeps base prompt tab at index 0).
1389
+ - Removes all UI config groups prefixed with 'agent.' (will be rebuilt).
1390
+ - Rebuilds tabs from the current provider registry order (provider.all()).
1391
+ - Applies saved values from current preset.extra[agent_id] when available; otherwise applies defaults.
1392
+ - Recomputes self.tab_options_idx from the real QTabWidget.
1393
+ - Restores visibility according to current mode/provider.
1394
+
1395
+ Notes:
1396
+ - Must be called in the Qt GUI thread.
1397
+ - Does not persist changes to preset files; it's a UI rebuild.
1398
+ - Set purge_missing_from_preset=True if you also want to drop missing agents from preset.extra.
1399
+ """
1400
+ tabs = self.window.ui.tabs['preset.editor.extra']
1401
+ if not tabs:
1402
+ return
1403
+
1404
+ # 0) Provider combos might need to be refreshed (e.g., new agents installed)
1405
+ self.update_providers_list()
1406
+
1407
+ # Make sure existing tabs have agent_id metadata (for older builds).
1408
+ self._ensure_agent_tab_properties()
1409
+
1410
+ mode = self.window.core.config.get('mode')
1411
+ exclude_ids = ["__prompt__"]
1412
+
1413
+ # 1) Snapshot saved values from current preset (if any)
1414
+ preset = self.window.core.presets.get_by_uuid(self.current)
1415
+ saved_all = {}
1416
+ if preset and preset.extra:
1417
+ # expected: preset.extra[agent_id][option_tab_id][key] = value
1418
+ # shallow copy is sufficient (we only read and apply)
1419
+ for a_id, data in preset.extra.items():
1420
+ if isinstance(data, dict):
1421
+ saved_all[a_id] = dict(data)
1422
+
1423
+ # 2) Freeze UI to avoid flicker while we rebuild
1424
+ tabs.setUpdatesEnabled(False)
1425
+ try:
1426
+ # 3) Remove all agent extra tabs (keep base prompt at 0)
1427
+ for i in range(tabs.count() - 1, 1 - 1, -1):
1428
+ w = tabs.widget(i)
1429
+ if w is None:
1430
+ continue
1431
+ if w.property('agent_id'):
1432
+ tabs.removeTab(i)
1433
+ w.deleteLater()
1434
+
1435
+ # 4) Remove all UI config groups for agents (they will be re-created)
1436
+ cfg = self.window.ui.config
1437
+ for key in list(cfg.keys()):
1438
+ if isinstance(key, str) and key.startswith('agent.'):
1439
+ try:
1440
+ del cfg[key]
1441
+ except Exception:
1442
+ pass
1443
+
1444
+ # 5) Optionally purge preset.extra entries for missing agents
1445
+ agents_dict = self.window.core.agents.provider.all() or {}
1446
+ existing_ids = set(agents_dict.keys())
1447
+ if purge_missing_from_preset and preset and isinstance(preset.extra, dict):
1448
+ for a_id in list(preset.extra.keys()):
1449
+ if a_id not in existing_ids:
1450
+ try:
1451
+ del preset.extra[a_id]
1452
+ except Exception:
1453
+ pass
1454
+ # You may call self.window.core.presets.save(preset.filename) later if you want to persist.
1455
+
1456
+ # 6) Rebuild tabs from current registry, in a deterministic order
1457
+ build_option_widgets = self.window.ui.dialogs.preset.build_option_widgets
1458
+ apply_value = self.window.controller.config.apply_value
1459
+
1460
+ insertion_index = 1 # index 0 is the base prompt
1461
+ for a_id, agent in agents_dict.items():
1462
+ if not agent:
1463
+ continue
1464
+ option_tabs = agent.get_options() or {}
1465
+ if not option_tabs:
1466
+ # agent with no custom tabs -> only base prompt applies
1467
+ continue
1468
+
1469
+ for option_tab_id, option_desc in option_tabs.items():
1470
+ if option_tab_id in exclude_ids:
1471
+ continue
1472
+
1473
+ title = option_desc.get('label', '')
1474
+ schema_options = option_desc.get('options', {}) or {}
1475
+ config_id = f"agent.{a_id}.{option_tab_id}"
1476
+
1477
+ # Build UI widgets for this option group
1478
+ widgets, options_layouts = build_option_widgets(config_id, schema_options)
1479
+
1480
+ # Layout: non-bool vertically, bools grouped in bottom row
1481
+ layout = QVBoxLayout()
1482
+ layout.setContentsMargins(0, 10, 0, 10)
1483
+ checkbox_layout = QHBoxLayout()
1484
+ for key, opt_layout in options_layouts.items():
1485
+ opt_schema = schema_options.get(key, {})
1486
+ if opt_schema.get('type') == 'bool':
1487
+ checkbox_layout.addLayout(opt_layout)
1488
+ else:
1489
+ layout.addLayout(opt_layout)
1490
+ layout.addStretch(1)
1491
+ layout.addLayout(checkbox_layout)
1492
+
1493
+ # Create tab widget and tag with metadata
1494
+ tab_widget = QWidget()
1495
+ tab_widget.setLayout(layout)
1496
+ tab_widget.setProperty('agent_id', a_id)
1497
+ tab_widget.setProperty('option_tab_id', option_tab_id)
1498
+
1499
+ # Insert tab and advance anchor
1500
+ insertion_index = min(insertion_index, tabs.count())
1501
+ tabs.insertTab(insertion_index, tab_widget, title)
1502
+ insertion_index += 1
1503
+
1504
+ # Apply saved values (if present) or defaults
1505
+ saved_agent = saved_all.get(a_id, {})
1506
+ saved_tab = saved_agent.get(option_tab_id, {}) if isinstance(saved_agent, dict) else {}
1507
+
1508
+ for key, opt_schema in schema_options.items():
1509
+ if key in saved_tab:
1510
+ apply_value(
1511
+ parent_id=config_id,
1512
+ key=key,
1513
+ option=opt_schema,
1514
+ value=saved_tab[key],
1515
+ )
1516
+ elif 'default' in opt_schema:
1517
+ apply_value(
1518
+ parent_id=config_id,
1519
+ key=key,
1520
+ option=opt_schema,
1521
+ value=opt_schema.get('default'),
1522
+ )
1523
+
1524
+ # 7) Recompute the index mapping strictly from the QTabWidget
1525
+ self._rebuild_tab_index_mapping()
1526
+
1527
+ # 8) Restore proper visibility for the current mode/provider
1528
+ self.toggle_extra_options_by_provider()
1529
+
1530
+ finally:
1531
+ tabs.setUpdatesEnabled(True)