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
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ================================================== #
4
+ # This file is a part of PYGPT package #
5
+ # Website: https://pygpt.net #
6
+ # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
+ # MIT License #
8
+ # Created By : Marcin Szczygliński #
9
+ # Updated Date: 2025.09.24 23:00:00 #
10
+ # ================================================== #
11
+
12
+ from __future__ import annotations
13
+ from typing import Any, Dict, Tuple, Optional, List
14
+
15
+ from pygpt_net.core.types import AGENT_TYPE_OPENAI, AGENT_MODE_OPENAI
16
+ from pygpt_net.item.ctx import CtxItem
17
+ from pygpt_net.item.model import ModelItem
18
+ from pygpt_net.item.preset import PresetItem
19
+
20
+ from pygpt_net.core.agents.bridge import ConnectionContext
21
+ from pygpt_net.core.bridge import BridgeContext
22
+
23
+ from agents import TResponseInputItem
24
+
25
+ from ..base import BaseAgent
26
+
27
+ from pygpt_net.core.agents.custom.logging import NullLogger, StdLogger
28
+ from pygpt_net.core.agents.custom.runner import FlowOrchestrator
29
+ from pygpt_net.core.agents.custom.utils import make_option_getter
30
+
31
+
32
+ class Agent(BaseAgent):
33
+ def __init__(self, *args, **kwargs):
34
+ super(Agent, self).__init__(*args, **kwargs)
35
+ self.id = "openai_custom"
36
+ self.type = AGENT_TYPE_OPENAI
37
+ self.mode = AGENT_MODE_OPENAI
38
+ self.name = "Custom"
39
+
40
+ async def run(
41
+ self,
42
+ window: Any = None,
43
+ agent_kwargs: Dict[str, Any] = None,
44
+ previous_response_id: str = None,
45
+ messages: List[TResponseInputItem] = None,
46
+ ctx: CtxItem = None,
47
+ stream: bool = False,
48
+ bridge: ConnectionContext = None,
49
+ use_partial_ctx: Optional[bool] = False,
50
+ schema: Optional[list] = None,
51
+ ) -> Tuple[CtxItem, str, str]:
52
+ agent_kwargs = agent_kwargs or {}
53
+ messages = messages or []
54
+
55
+ context: BridgeContext = agent_kwargs.get("context", BridgeContext())
56
+ preset: Optional[PresetItem] = context.preset
57
+ model: ModelItem = agent_kwargs.get("model", ModelItem())
58
+ function_tools: list = agent_kwargs.get("function_tools", [])
59
+
60
+ base_prompt = self.get_option(preset, "base", "prompt")
61
+ allow_local_tools_default = bool(self.get_option(preset, "base", "allow_local_tools"))
62
+ allow_remote_tools_default = bool(self.get_option(preset, "base", "allow_remote_tools"))
63
+ max_iterations = int(self.get_option(preset, "base", "max_iterations") or agent_kwargs.get("max_iterations", 20))
64
+ trace_id = self.get_option(preset, "debug", "trace_id") or agent_kwargs.get("trace_id", None)
65
+ router_stream_mode = self.get_option(preset, "router", "stream_mode") or agent_kwargs.get("router_stream_mode", "realtime")
66
+ verbose = bool(agent_kwargs.get("verbose", False))
67
+ logger = StdLogger(prefix="[flow]") if verbose else NullLogger()
68
+ option_get = make_option_getter(self, preset)
69
+
70
+ orchestrator = FlowOrchestrator(window=window, logger=logger)
71
+ logger.debug(f"[schema] {schema}")
72
+
73
+ result = await orchestrator.run_flow(
74
+ schema=schema or [],
75
+ messages=messages,
76
+ ctx=ctx,
77
+ bridge=bridge,
78
+ agent_kwargs=agent_kwargs,
79
+ preset=preset,
80
+ model=model,
81
+ stream=stream,
82
+ use_partial_ctx=use_partial_ctx or False,
83
+ base_prompt=base_prompt,
84
+ allow_local_tools_default=allow_local_tools_default,
85
+ allow_remote_tools_default=allow_remote_tools_default,
86
+ function_tools=function_tools,
87
+ trace_id=trace_id,
88
+ max_iterations=max_iterations,
89
+ router_stream_mode=str(router_stream_mode).lower(),
90
+ option_get=option_get,
91
+ )
92
+
93
+ final_output = result.final_output or ""
94
+ last_response_id = result.last_response_id or ""
95
+
96
+ return result.ctx, final_output, last_response_id
@@ -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.09.19 00:00:00 #
9
+ # Updated Date: 2025.09.24 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import json
@@ -60,7 +60,10 @@ class JsonFileProvider(BaseProvider):
60
60
  try:
61
61
  if os.path.exists(path):
62
62
  with open(path, 'r', encoding="utf-8") as file:
63
- data = json.load(file)
63
+ try:
64
+ data = json.load(file)
65
+ except json.JSONDecodeError:
66
+ data = {}
64
67
  if data == "" or data is None:
65
68
  return {}
66
69
  if "layout" in data:
@@ -143,6 +146,7 @@ class JsonFileProvider(BaseProvider):
143
146
  'id': item.id,
144
147
  'name': item.name,
145
148
  'layout': item.layout,
149
+ 'schema': item.schema,
146
150
  }
147
151
 
148
152
  @staticmethod
@@ -159,6 +163,8 @@ class JsonFileProvider(BaseProvider):
159
163
  item.name = data['name']
160
164
  if "layout" in data:
161
165
  item.layout = data['layout']
166
+ if "schema" in data:
167
+ item.schema = data['schema']
162
168
 
163
169
  @staticmethod
164
170
  def serialize_layout(item: BuilderLayoutItem) -> Dict[str, Any]:
@@ -169,9 +175,9 @@ class JsonFileProvider(BaseProvider):
169
175
  :return: serialized item
170
176
  """
171
177
  return {
172
- 'id': item.id,
173
- 'name': item.name,
174
- 'data': item.data,
178
+ 'id': item.id if item else "",
179
+ 'name': item.name if item else "",
180
+ 'data': item.data if item else {},
175
181
  }
176
182
 
177
183
  @staticmethod
@@ -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.09.22 19:00:00 #
9
+ # Updated Date: 2025.09.26 03:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
@@ -151,6 +151,15 @@ class Patch:
151
151
  data["ctx.urls.internal"] = False
152
152
  updated = True
153
153
 
154
+ # < 2.6.61
155
+ if old < parse_version("2.6.61"):
156
+ print("Migrating config from < 2.6.61...")
157
+ if "presets.drag_and_drop.enabled" not in data:
158
+ data["presets.drag_and_drop.enabled"] = True
159
+ if "presets_order" not in data:
160
+ data["presets_order"] = {}
161
+ updated = True
162
+
154
163
  # update file
155
164
  migrated = False
156
165
  if updated:
@@ -367,8 +367,6 @@ class Patch:
367
367
  # < 2.0.72
368
368
  if old < parse_version("2.0.72"):
369
369
  print("Migrating config from < 2.0.72...")
370
- if 'theme.markdown' not in data:
371
- data['theme.markdown'] = True
372
370
  prompt = 'IMAGE GENERATION: Whenever I provide a basic idea or concept for an image, such as \'a picture of ' \
373
371
  'mountains\', I want you to ALWAYS translate it into English and expand and elaborate on this idea. ' \
374
372
  'Use your knowledge and creativity to add details that would make the image more vivid and ' \
@@ -1301,8 +1299,6 @@ class Patch:
1301
1299
  data["render.code_syntax"] = "github-dark"
1302
1300
  if 'zoom' not in data:
1303
1301
  data["zoom"] = 1.0
1304
- if 'ctx.convert_lists' not in data:
1305
- data["ctx.convert_lists"] = False
1306
1302
  if 'render.engine' not in data:
1307
1303
  data["render.engine"] = "web"
1308
1304
  if 'render.open_gl' not in data:
@@ -1668,8 +1664,6 @@ class Patch:
1668
1664
  # < 2.4.19
1669
1665
  if old < parse_version("2.4.19"):
1670
1666
  print("Migrating config from < 2.4.19...")
1671
- if 'layout.animation.disable' not in data:
1672
- data["layout.animation.disable"] = cfg_get_base('layout.animation.disable')
1673
1667
  if 'cmd_code_interpreter' in data['plugins'] \
1674
1668
  and 'cmd.ipython_execute' in data['plugins']['cmd_code_interpreter']:
1675
1669
  # remove
@@ -6,9 +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: 2025.09.19 00:00:00 #
9
+ # Updated Date: 2025.09.25 14:00:00 #
10
10
  # ================================================== #
11
11
 
12
+ import copy
12
13
  from datetime import datetime
13
14
  from typing import Dict
14
15
 
@@ -18,21 +19,26 @@ from PySide6.QtGui import QAction, QIcon
18
19
  from pygpt_net.tools.base import BaseTool
19
20
  from pygpt_net.utils import trans
20
21
 
22
+ from pygpt_net.core.node_editor.types import (
23
+ NodeTypeRegistry,
24
+ PropertySpec,
25
+ NodeTypeSpec
26
+ )
27
+
21
28
  from .ui.dialogs import Builder
22
29
 
30
+
23
31
  class AgentBuilder(BaseTool):
24
32
  def __init__(self, *args, **kwargs):
25
- """
26
- Agent builder (nodes editor)
27
-
28
- :param window: Window instance
29
- """
30
33
  super(AgentBuilder, self).__init__(*args, **kwargs)
31
34
  self.id = "agent_builder"
32
35
  self.opened = False
33
36
  self.dialog = None # dialog
34
37
  self.initialized = False
35
38
  self.current_agent = None # current agent ID
39
+ # Reentrancy/teardown guards
40
+ self._restoring = False # True while restore/load is in progress
41
+ self._closing = False # True while dialog is being closed
36
42
 
37
43
  def setup(self):
38
44
  """Setup controller"""
@@ -42,17 +48,41 @@ class AgentBuilder(BaseTool):
42
48
  """Update"""
43
49
  pass
44
50
 
45
- def save(self):
51
+ def save(self, force: bool = False):
46
52
  """Save layout to file"""
53
+ # Never save during restore or teardown
54
+ if not self.opened or self._closing or self._restoring:
55
+ if not force:
56
+ return
47
57
  if "agent.builder" not in self.window.ui.editor:
48
58
  return
59
+
60
+ builder = self.window.ui.editor["agent.builder"]
61
+ # Ensure the editor is alive (not closing)
62
+ if not getattr(builder, "_alive", True) or getattr(builder, "_closing", False):
63
+ return
64
+
65
+ data = builder.save_layout()
66
+ if not isinstance(data, dict):
67
+ return # skip invalid snapshots
68
+
69
+ schema_list = builder.export_schema(as_list=True)
70
+ try:
71
+ # Deep copy for safety (JSON-only types expected)
72
+ layout = copy.deepcopy(data)
73
+ schema = copy.deepcopy(schema_list)
74
+ except Exception:
75
+ # As a fallback, store raw references (still pure dict/list)
76
+ layout = data
77
+ schema = schema_list
78
+
49
79
  custom = self.window.core.agents.custom
50
- layout = self.window.ui.editor["agent.builder"].save_layout()
51
80
  custom.update_layout(layout)
52
81
  if self.current_agent:
53
- custom.update_agent_layout(self.current_agent, layout)
82
+ custom.update_agent(self.current_agent, layout, schema)
54
83
  custom.save()
55
84
  self.window.update_status(f"Saved at: {datetime.now().strftime('%H:%M:%S')}")
85
+ self.update_presets()
56
86
 
57
87
  def load(self):
58
88
  """Load layout from file"""
@@ -60,11 +90,10 @@ class AgentBuilder(BaseTool):
60
90
 
61
91
  def open(self):
62
92
  """Open dialog"""
63
- if not self.initialized:
64
- self.dialog = Builder(self.window, tool=self)
65
- self.dialog.setup()
66
- self.restore()
67
- self.initialized = True
93
+ self.dialog = Builder(self.window, tool=self)
94
+ self.dialog.setup()
95
+ # Defer restore to allow previous deleteLater() to be processed
96
+ QtCore.QTimer.singleShot(0, self.restore)
68
97
  self.window.ui.dialogs.open('agent.builder', width=900, height=600)
69
98
  self.opened = True
70
99
  self.update()
@@ -81,26 +110,33 @@ class AgentBuilder(BaseTool):
81
110
  else:
82
111
  self.open()
83
112
 
84
- def store_current(self):
113
+ def store_current(self, force: bool = False):
85
114
  """Store current to file"""
86
- self.save()
115
+ if self.opened and not self._restoring and (not self._closing or force):
116
+ self.save(force=force)
87
117
 
88
118
  def restore(self):
89
119
  """Restore layout and agents from file"""
90
- layout = self.window.core.agents.custom.get_layout()
91
- if layout:
92
- data = layout.data
93
- if data:
94
- self.window.ui.editor["agent.builder"].load_layout(data)
95
- self.window.update_status(f"Loaded layout at: {datetime.now().strftime('%H:%M:%S')}")
120
+ if self._closing:
121
+ return
122
+ self._restoring = True
123
+ try:
124
+ layout = self.window.core.agents.custom.get_layout()
125
+ if layout and "agent.builder" in self.window.ui.editor:
126
+ data = layout.data
127
+ if data:
128
+ self.window.ui.editor["agent.builder"].load_layout(data)
129
+ self.window.update_status(f"Loaded layout at: {datetime.now().strftime('%H:%M:%S')}")
96
130
 
97
- self.update_list()
131
+ self.update_list()
98
132
 
99
- # select first agent on list
100
- agents = self.window.core.agents.custom.get_agents()
101
- if agents and len(agents) > 0:
102
- first_agent_id = list(agents.keys())[0]
103
- self.edit_agent(first_agent_id)
133
+ # select first agent on list
134
+ agents = self.window.core.agents.custom.get_agents()
135
+ if agents and len(agents) > 0:
136
+ first_agent_id = list(agents.keys())[0]
137
+ self.edit_agent(first_agent_id)
138
+ finally:
139
+ self._restoring = False
104
140
 
105
141
  def show_hide(self, show: bool = True):
106
142
  """
@@ -114,13 +150,18 @@ class AgentBuilder(BaseTool):
114
150
  self.close()
115
151
 
116
152
  def on_close(self):
117
- """On window close"""
118
- self.opened = False
119
- self.update()
153
+ """On dialog window close (before destroy)"""
154
+ self._closing = True
155
+ try:
156
+ self.store_current(force=True)
157
+ finally:
158
+ self.opened = False
159
+ self._closing = False
120
160
 
121
161
  def on_exit(self):
122
- """On exit"""
123
- self.store_current()
162
+ """On app exit"""
163
+ if self.opened:
164
+ self.on_close()
124
165
 
125
166
  def clear(self, force: bool = False):
126
167
  """
@@ -135,24 +176,33 @@ class AgentBuilder(BaseTool):
135
176
  msg=trans('agent.builder.confirm.clear.msg'),
136
177
  )
137
178
  return
138
- if self.window.ui.editor["agent.builder"].clear(ask_user=False):
139
- self.save()
179
+ if "agent.builder" in self.window.ui.editor:
180
+ if self.window.ui.editor["agent.builder"].clear(ask_user=False):
181
+ self.save()
140
182
 
141
183
  def add_agent(self, name: str = None):
142
184
  """Add new agent"""
143
185
  if name is None:
144
- self.window.ui.dialog['create'].id = 'agent.builder.agent'
145
- self.window.ui.dialog['create'].input.setText("")
146
- self.window.ui.dialog['create'].current = ""
147
- self.window.ui.dialog['create'].show()
148
- self.window.ui.dialog['create'].input.setFocus()
186
+ dialog = self.window.ui.dialog['create']
187
+ dialog.id = 'agent.builder.agent'
188
+ dialog.input.setText("")
189
+ dialog.current = ""
190
+ dialog.show()
191
+ dialog.input.setFocus()
149
192
  return
193
+
194
+ if self.current_agent:
195
+ self.save() # save current before creating new
196
+
197
+ self.current_agent = None # prevent overwriting current agent
150
198
  uuid = self.window.core.agents.custom.new_agent(name)
151
199
  if uuid:
152
200
  self.update_list()
153
- self.window.ui.editor["agent.builder"].clear(ask_user=False)
201
+ if "agent.builder" in self.window.ui.editor:
202
+ self.window.ui.editor["agent.builder"].clear(ask_user=False)
154
203
  self.window.ui.dialogs.close('create')
155
204
  self.edit_agent(uuid)
205
+ self.update_presets()
156
206
 
157
207
  def rename_agent(self, agent_id: str, name: str = None):
158
208
  """
@@ -167,17 +217,19 @@ class AgentBuilder(BaseTool):
167
217
  return
168
218
  current_name = agent.name
169
219
  if name is None:
170
- self.window.ui.dialog['rename'].id = 'agent.builder.agent'
171
- self.window.ui.dialog['rename'].input.setText(current_name)
172
- self.window.ui.dialog['rename'].current =agent_id
173
- self.window.ui.dialog['rename'].show()
174
- self.window.ui.dialog['rename'].input.setFocus()
220
+ dialog = self.window.ui.dialog['rename']
221
+ dialog.id = 'agent.builder.agent'
222
+ dialog.input.setText(current_name)
223
+ dialog.current =agent_id
224
+ dialog.show()
225
+ dialog.input.setFocus()
175
226
  return
176
227
  if agent:
177
228
  agent.name = name
178
229
  self.window.core.agents.custom.save()
179
230
  self.update_list()
180
231
  self.window.ui.dialogs.close('rename')
232
+ self.update_presets()
181
233
 
182
234
  def edit_agent(self, agent_id: str):
183
235
  """
@@ -185,16 +237,36 @@ class AgentBuilder(BaseTool):
185
237
 
186
238
  :param agent_id: agent ID
187
239
  """
188
- self.save()
240
+ # Save current agent only when switching and UI is stable
241
+ if (
242
+ self.current_agent
243
+ and self.current_agent != agent_id
244
+ and self.opened
245
+ and not self._restoring
246
+ and not self._closing
247
+ and "agent.builder" in self.window.ui.editor
248
+ ):
249
+ builder = self.window.ui.editor["agent.builder"]
250
+ if getattr(builder, "_alive", True) and not getattr(builder, "_closing", False):
251
+ self.save()
252
+
189
253
  self.current_agent = agent_id
254
+
255
+ if "agent.builder" not in self.window.ui.editor:
256
+ return
257
+
190
258
  agent = self.window.core.agents.custom.get_agent(agent_id)
191
259
  if agent:
260
+ editor = self.window.ui.editor["agent.builder"]
192
261
  layout = agent.layout
193
262
  if layout:
194
- self.window.ui.editor["agent.builder"].load_layout(layout)
263
+ editor.load_layout(layout)
195
264
  else:
196
- self.window.ui.editor["agent.builder"].clear(ask_user=False)
197
- self.select_on_list(agent_id)
265
+ editor.clear(ask_user=False)
266
+
267
+ # Select on list only when the list exists (dialog open)
268
+ if "agent.builder.list" in self.window.ui.nodes:
269
+ self.select_on_list(agent_id)
198
270
 
199
271
  def duplicate_agent(self, agent_id: str):
200
272
  """
@@ -224,12 +296,39 @@ class AgentBuilder(BaseTool):
224
296
  return
225
297
  self.window.core.agents.custom.delete_agent(agent_id)
226
298
  self.update_list()
299
+
300
+ # if deleted current, clear editor
227
301
  if self.current_agent == agent_id:
228
302
  self.current_agent = None
229
- self.window.ui.editor["agent.builder"].clear(ask_user=False)
303
+ if "agent.builder" in self.window.ui.editor:
304
+ self.window.ui.editor["agent.builder"].clear(ask_user=False)
305
+
306
+ # select current selected on list if any after deletion (only if UI is open)
307
+ if self.opened and "agent.builder.list" in self.window.ui.nodes:
308
+ agents = self.window.core.agents.custom.get_agents()
309
+ if agents and len(agents) > 0:
310
+ idx = self.window.ui.nodes["agent.builder.list"].list.currentIndex()
311
+ if idx.isValid():
312
+ next_id = idx.data(QtCore.Qt.UserRole)
313
+ self.edit_agent(next_id)
314
+
315
+ def editing_allowed(self) -> bool:
316
+ """
317
+ Check if editing is enabled (dialog is open and not closing/restoring)
318
+
319
+ :return: True if editing is enabled
320
+ """
321
+ return self.opened and not self._closing and not self._restoring and self.current_agent is not None
322
+
323
+ def update_presets(self):
324
+ """Update presets in the tools"""
325
+ self.window.controller.presets.editor.reload_all(all=True)
230
326
 
231
327
  def update_list(self):
232
328
  """Update agents list"""
329
+ # Guard: dialog may not be open; do nothing if widget is not present
330
+ if "agent.builder.list" not in self.window.ui.nodes:
331
+ return
233
332
  data = self.window.core.agents.custom.get_agents()
234
333
  self.window.ui.nodes["agent.builder.list"].update_list(data)
235
334
 
@@ -239,13 +338,16 @@ class AgentBuilder(BaseTool):
239
338
 
240
339
  :param agent_id: agent ID
241
340
  """
341
+ if "agent.builder.list" not in self.window.ui.nodes:
342
+ return
343
+
242
344
  nodes = self.window.ui.nodes
243
345
  models = self.window.ui.models
244
346
 
245
347
  agents_list = nodes["agent.builder.list"].list
246
348
  model = models.get("agent.builder.list")
247
349
 
248
- if model is None:
350
+ if model is None or agents_list is None:
249
351
  return
250
352
  for row in range(model.rowCount()):
251
353
  idx = model.index(row, 0)
@@ -279,6 +381,85 @@ class AgentBuilder(BaseTool):
279
381
  """Setup dialogs (static)"""
280
382
  pass
281
383
 
384
+ def get_registry(self) -> NodeTypeRegistry:
385
+ """
386
+ Get node type registry
387
+
388
+ :return: NodeTypeRegistry
389
+ """
390
+ registry = NodeTypeRegistry(empty=True)
391
+
392
+ # Start
393
+ registry.register(NodeTypeSpec(
394
+ type_name="Flow/Start",
395
+ title=trans("node.editor.spec.start.title"),
396
+ base_id="start",
397
+ export_kind="start",
398
+ bg_color="#2D5A27",
399
+ properties=[
400
+ PropertySpec(id="output", type="flow", name=trans("node.editor.property.output.name"), editable=False,
401
+ allowed_inputs=0, allowed_outputs=1),
402
+ PropertySpec(id="memory", type="memory", name=trans("node.editor.property.memory.name"), editable=False,
403
+ allowed_inputs=0, allowed_outputs=-1),
404
+ ],
405
+ ))
406
+ # Agent
407
+ registry.register(NodeTypeSpec(
408
+ type_name="Flow/Agent",
409
+ title=trans("node.editor.spec.agent.title"),
410
+ base_id="agent",
411
+ export_kind="agent",
412
+ bg_color="#304A6E",
413
+ properties=[
414
+ PropertySpec(id="name", type="str", name=trans("node.editor.property.name.name"), editable=True,
415
+ value="",
416
+ placeholder=trans("node.editor.property.name.placeholder")),
417
+ PropertySpec(id="role", type="str", name=trans("node.editor.property.role.name"), editable=True,
418
+ value="",
419
+ placeholder=trans("node.editor.property.role.placeholder")),
420
+ PropertySpec(id="instruction", type="text", name=trans("node.editor.property.instruction.name"),
421
+ editable=True, value="",
422
+ placeholder=trans("node.editor.property.instruction.placeholder")),
423
+ PropertySpec(id="remote_tools", type="bool", name=trans("node.editor.property.remote_tools.name"),
424
+ editable=True, value=True),
425
+ PropertySpec(id="local_tools", type="bool", name=trans("node.editor.property.local_tools.name"),
426
+ editable=True, value=True),
427
+ PropertySpec(id="input", type="flow", name=trans("node.editor.property.input.name"), editable=False,
428
+ allowed_inputs=-1, allowed_outputs=0),
429
+ PropertySpec(id="output", type="flow", name=trans("node.editor.property.output.name"), editable=False,
430
+ allowed_inputs=0, allowed_outputs=-1),
431
+ PropertySpec(id="memory", type="memory", name=trans("node.editor.property.memory.name"), editable=False,
432
+ allowed_inputs=0, allowed_outputs=1),
433
+ ],
434
+ ))
435
+ # Memory
436
+ registry.register(NodeTypeSpec(
437
+ type_name="Flow/Memory",
438
+ title=trans("node.editor.spec.memory.title"),
439
+ base_id="mem",
440
+ export_kind="memory",
441
+ bg_color="#593E78",
442
+ properties=[
443
+ PropertySpec(id="name", type="str", name=trans("node.editor.property.name.name"), editable=True,
444
+ value=""),
445
+ PropertySpec(id="input", type="memory", name=trans("node.editor.property.agent.name"), editable=False,
446
+ allowed_inputs=-1, allowed_outputs=0),
447
+ ],
448
+ ))
449
+ # End
450
+ registry.register(NodeTypeSpec(
451
+ type_name="Flow/End",
452
+ title=trans("node.editor.spec.end.title"),
453
+ base_id="end",
454
+ export_kind="end",
455
+ bg_color="#6B2E2E",
456
+ properties=[
457
+ PropertySpec(id="input", type="flow", name=trans("node.editor.property.input.name"), editable=False,
458
+ allowed_inputs=-1, allowed_outputs=0),
459
+ ],
460
+ ))
461
+ return registry
462
+
282
463
  def get_lang_mappings(self) -> Dict[str, Dict]:
283
464
  """
284
465
  Get language mappings