pygpt-net 2.6.59__py3-none-any.whl → 2.6.60__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.
- pygpt_net/CHANGELOG.txt +4 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +9 -5
- pygpt_net/controller/__init__.py +1 -0
- pygpt_net/controller/presets/editor.py +442 -39
- pygpt_net/core/agents/custom/__init__.py +275 -0
- pygpt_net/core/agents/custom/debug.py +64 -0
- pygpt_net/core/agents/custom/factory.py +109 -0
- pygpt_net/core/agents/custom/graph.py +71 -0
- pygpt_net/core/agents/custom/llama_index/__init__.py +10 -0
- pygpt_net/core/agents/custom/llama_index/factory.py +89 -0
- pygpt_net/core/agents/custom/llama_index/router_streamer.py +106 -0
- pygpt_net/core/agents/custom/llama_index/runner.py +529 -0
- pygpt_net/core/agents/custom/llama_index/stream.py +56 -0
- pygpt_net/core/agents/custom/llama_index/utils.py +242 -0
- pygpt_net/core/agents/custom/logging.py +50 -0
- pygpt_net/core/agents/custom/memory.py +51 -0
- pygpt_net/core/agents/custom/router.py +116 -0
- pygpt_net/core/agents/custom/router_streamer.py +187 -0
- pygpt_net/core/agents/custom/runner.py +454 -0
- pygpt_net/core/agents/custom/schema.py +125 -0
- pygpt_net/core/agents/custom/utils.py +181 -0
- pygpt_net/core/agents/provider.py +72 -7
- pygpt_net/core/agents/runner.py +7 -4
- pygpt_net/core/agents/runners/helpers.py +1 -1
- pygpt_net/core/agents/runners/llama_workflow.py +3 -0
- pygpt_net/core/agents/runners/openai_workflow.py +8 -1
- pygpt_net/{ui/widget/builder → core/node_editor}/__init__.py +2 -2
- pygpt_net/core/{builder → node_editor}/graph.py +11 -218
- pygpt_net/core/node_editor/models.py +111 -0
- pygpt_net/core/node_editor/types.py +76 -0
- pygpt_net/core/node_editor/utils.py +17 -0
- pygpt_net/core/render/web/renderer.py +10 -8
- pygpt_net/data/config/config.json +3 -3
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/locale/locale.en.ini +4 -4
- pygpt_net/item/agent.py +5 -1
- pygpt_net/item/preset.py +19 -1
- pygpt_net/provider/agents/base.py +33 -2
- pygpt_net/provider/agents/llama_index/flow_from_schema.py +92 -0
- pygpt_net/provider/agents/openai/flow_from_schema.py +96 -0
- pygpt_net/provider/core/agent/json_file.py +11 -5
- pygpt_net/tools/agent_builder/tool.py +217 -52
- pygpt_net/tools/agent_builder/ui/dialogs.py +119 -24
- pygpt_net/tools/agent_builder/ui/list.py +37 -10
- pygpt_net/ui/dialog/preset.py +16 -1
- pygpt_net/ui/main.py +1 -1
- pygpt_net/{core/builder → ui/widget/node_editor}/__init__.py +2 -2
- pygpt_net/ui/widget/node_editor/command.py +373 -0
- pygpt_net/ui/widget/node_editor/editor.py +2038 -0
- pygpt_net/ui/widget/node_editor/item.py +492 -0
- pygpt_net/ui/widget/node_editor/node.py +1205 -0
- pygpt_net/ui/widget/node_editor/utils.py +17 -0
- pygpt_net/ui/widget/node_editor/view.py +247 -0
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.60.dist-info}/METADATA +72 -2
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.60.dist-info}/RECORD +59 -33
- pygpt_net/core/agents/custom.py +0 -150
- pygpt_net/ui/widget/builder/editor.py +0 -2001
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.60.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.60.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.60.dist-info}/entry_points.txt +0 -0
|
@@ -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.
|
|
9
|
+
# Updated Date: 2025.09.24 00: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.
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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.
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
131
|
+
self.update_list()
|
|
98
132
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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.
|
|
119
|
-
|
|
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.
|
|
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
|
|
139
|
-
self.
|
|
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']
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
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']
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
263
|
+
editor.load_layout(layout)
|
|
195
264
|
else:
|
|
196
|
-
|
|
197
|
-
|
|
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,31 @@ 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
|
|
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 update_presets(self):
|
|
316
|
+
"""Update presets in the tools"""
|
|
317
|
+
self.window.controller.presets.editor.reload_all(all=True)
|
|
230
318
|
|
|
231
319
|
def update_list(self):
|
|
232
320
|
"""Update agents list"""
|
|
321
|
+
# Guard: dialog may not be open; do nothing if widget is not present
|
|
322
|
+
if "agent.builder.list" not in self.window.ui.nodes:
|
|
323
|
+
return
|
|
233
324
|
data = self.window.core.agents.custom.get_agents()
|
|
234
325
|
self.window.ui.nodes["agent.builder.list"].update_list(data)
|
|
235
326
|
|
|
@@ -239,13 +330,16 @@ class AgentBuilder(BaseTool):
|
|
|
239
330
|
|
|
240
331
|
:param agent_id: agent ID
|
|
241
332
|
"""
|
|
333
|
+
if "agent.builder.list" not in self.window.ui.nodes:
|
|
334
|
+
return
|
|
335
|
+
|
|
242
336
|
nodes = self.window.ui.nodes
|
|
243
337
|
models = self.window.ui.models
|
|
244
338
|
|
|
245
339
|
agents_list = nodes["agent.builder.list"].list
|
|
246
340
|
model = models.get("agent.builder.list")
|
|
247
341
|
|
|
248
|
-
if model is None:
|
|
342
|
+
if model is None or agents_list is None:
|
|
249
343
|
return
|
|
250
344
|
for row in range(model.rowCount()):
|
|
251
345
|
idx = model.index(row, 0)
|
|
@@ -279,6 +373,77 @@ class AgentBuilder(BaseTool):
|
|
|
279
373
|
"""Setup dialogs (static)"""
|
|
280
374
|
pass
|
|
281
375
|
|
|
376
|
+
def get_registry(self) -> NodeTypeRegistry:
|
|
377
|
+
"""
|
|
378
|
+
Get node type registry
|
|
379
|
+
|
|
380
|
+
:return: NodeTypeRegistry
|
|
381
|
+
"""
|
|
382
|
+
registry = NodeTypeRegistry(empty=True)
|
|
383
|
+
# Tip: to allow multiple connections to an input or output, set allowed_inputs/allowed_outputs to -1.
|
|
384
|
+
|
|
385
|
+
# Start
|
|
386
|
+
registry.register(NodeTypeSpec(
|
|
387
|
+
type_name="Flow/Start",
|
|
388
|
+
title="Start",
|
|
389
|
+
base_id="start",
|
|
390
|
+
export_kind="start",
|
|
391
|
+
bg_color="#2D5A27",
|
|
392
|
+
properties=[
|
|
393
|
+
PropertySpec(id="output", type="flow", name="Output", editable=False, allowed_inputs=0,
|
|
394
|
+
allowed_outputs=1),
|
|
395
|
+
PropertySpec(id="memory", type="memory", name="Memory", editable=False, allowed_inputs=0,
|
|
396
|
+
allowed_outputs=-1),
|
|
397
|
+
# base_id will be auto-injected as read-only property at creation
|
|
398
|
+
],
|
|
399
|
+
))
|
|
400
|
+
# Agent
|
|
401
|
+
registry.register(NodeTypeSpec(
|
|
402
|
+
type_name="Flow/Agent",
|
|
403
|
+
title="Agent",
|
|
404
|
+
base_id="agent",
|
|
405
|
+
export_kind="agent",
|
|
406
|
+
bg_color="#304A6E",
|
|
407
|
+
properties=[
|
|
408
|
+
PropertySpec(id="name", type="str", name="Name", editable=True, value=""),
|
|
409
|
+
PropertySpec(id="instruction", type="text", name="Instruction", editable=True, value=""),
|
|
410
|
+
PropertySpec(id="remote_tools", type="bool", name="Remote tools", editable=True, value=True),
|
|
411
|
+
PropertySpec(id="local_tools", type="bool", name="Local tools", editable=True, value=True),
|
|
412
|
+
PropertySpec(id="input", type="flow", name="Input", editable=False, allowed_inputs=-1,
|
|
413
|
+
allowed_outputs=0),
|
|
414
|
+
PropertySpec(id="output", type="flow", name="Output", editable=False, allowed_inputs=0,
|
|
415
|
+
allowed_outputs=-1),
|
|
416
|
+
PropertySpec(id="memory", type="memory", name="Memory", editable=False, allowed_inputs=0,
|
|
417
|
+
allowed_outputs=1),
|
|
418
|
+
],
|
|
419
|
+
))
|
|
420
|
+
# Memory
|
|
421
|
+
registry.register(NodeTypeSpec(
|
|
422
|
+
type_name="Flow/Memory",
|
|
423
|
+
title="Memory (Context)",
|
|
424
|
+
base_id="mem",
|
|
425
|
+
export_kind="memory",
|
|
426
|
+
bg_color="#593E78",
|
|
427
|
+
properties=[
|
|
428
|
+
PropertySpec(id="name", type="str", name="Name", editable=True, value=""),
|
|
429
|
+
PropertySpec(id="input", type="memory", name="Agent", editable=False, allowed_inputs=-1,
|
|
430
|
+
allowed_outputs=0),
|
|
431
|
+
],
|
|
432
|
+
))
|
|
433
|
+
# End
|
|
434
|
+
registry.register(NodeTypeSpec(
|
|
435
|
+
type_name="Flow/End",
|
|
436
|
+
title="End",
|
|
437
|
+
base_id="end",
|
|
438
|
+
export_kind="end",
|
|
439
|
+
bg_color="#6B2E2E",
|
|
440
|
+
properties=[
|
|
441
|
+
PropertySpec(id="input", type="flow", name="Input", editable=False, allowed_inputs=-1,
|
|
442
|
+
allowed_outputs=0),
|
|
443
|
+
],
|
|
444
|
+
))
|
|
445
|
+
return registry
|
|
446
|
+
|
|
282
447
|
def get_lang_mappings(self) -> Dict[str, Dict]:
|
|
283
448
|
"""
|
|
284
449
|
Get language mappings
|
|
@@ -6,18 +6,20 @@
|
|
|
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.
|
|
9
|
+
# Updated Date: 2025.09.24 00:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
|
-
from PySide6.QtCore import Qt
|
|
12
|
+
from PySide6.QtCore import Qt, QEvent
|
|
13
13
|
from PySide6.QtGui import QAction, QIcon
|
|
14
|
-
from PySide6.QtWidgets import QVBoxLayout, QMenuBar, QSplitter
|
|
14
|
+
from PySide6.QtWidgets import QVBoxLayout, QMenuBar, QSplitter, QSizePolicy
|
|
15
15
|
|
|
16
|
-
from pygpt_net.
|
|
17
|
-
from pygpt_net.ui.widget.
|
|
16
|
+
from pygpt_net.ui.widget.element.labels import HelpLabel
|
|
17
|
+
from pygpt_net.ui.widget.node_editor.editor import NodeEditor
|
|
18
18
|
from pygpt_net.ui.widget.dialog.base import BaseDialog
|
|
19
19
|
from pygpt_net.utils import trans
|
|
20
20
|
|
|
21
|
+
from .list import AgentsWidget
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
class Builder:
|
|
23
25
|
|
|
@@ -34,12 +36,12 @@ class Builder:
|
|
|
34
36
|
self.actions = {}
|
|
35
37
|
|
|
36
38
|
def setup_menu(self, parent=None) -> QMenuBar:
|
|
37
|
-
"""Setup
|
|
39
|
+
"""Setup agent_builder dialog menu"""
|
|
38
40
|
self.menu_bar = QMenuBar(parent)
|
|
39
41
|
self.file_menu = self.menu_bar.addMenu(trans("menu.file"))
|
|
40
42
|
t = self.tool
|
|
41
43
|
|
|
42
|
-
self.actions["open"] = QAction(QIcon(":/icons/
|
|
44
|
+
self.actions["open"] = QAction(QIcon(":/icons/reload.svg"), "Reload", self.menu_bar)
|
|
43
45
|
self.actions["open"].triggered.connect(lambda checked=False, t=t: t.load())
|
|
44
46
|
|
|
45
47
|
self.actions["save"] = QAction(QIcon(":/icons/save.svg"), trans("action.save"), self.menu_bar)
|
|
@@ -55,36 +57,44 @@ class Builder:
|
|
|
55
57
|
|
|
56
58
|
def setup(self):
|
|
57
59
|
"""Setup agent_builder dialog"""
|
|
60
|
+
# Ensure previous deferred deletions are flushed before building a new editor instance
|
|
61
|
+
try:
|
|
62
|
+
from PySide6.QtWidgets import QApplication
|
|
63
|
+
QApplication.sendPostedEvents(None, QEvent.DeferredDelete)
|
|
64
|
+
QApplication.processEvents()
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
|
|
58
68
|
id = 'agent.builder'
|
|
59
69
|
u = self.window.ui
|
|
60
70
|
|
|
61
71
|
u.dialog['agent.builder'] = BuilderDialog(self.window)
|
|
62
72
|
dlg = u.dialog['agent.builder']
|
|
63
73
|
|
|
64
|
-
|
|
74
|
+
registry = self.tool.get_registry() # node types
|
|
75
|
+
editor = NodeEditor(
|
|
76
|
+
parent=dlg,
|
|
77
|
+
registry=registry
|
|
78
|
+
) # parent == dialog
|
|
79
|
+
|
|
65
80
|
editor.setStyleSheet("""
|
|
66
81
|
NodeEditor {
|
|
67
82
|
qproperty-gridBackColor: #242629;
|
|
68
83
|
qproperty-gridPenColor: #3b3f46;
|
|
69
|
-
|
|
84
|
+
|
|
70
85
|
qproperty-nodeBackgroundColor: #2d2f34;
|
|
71
86
|
qproperty-nodeBorderColor: #4b4f57;
|
|
72
87
|
qproperty-nodeSelectionColor: #ff9900;
|
|
73
88
|
qproperty-nodeTitleColor: #3a3d44;
|
|
74
|
-
|
|
89
|
+
|
|
75
90
|
qproperty-portInputColor: #66b2ff;
|
|
76
91
|
qproperty-portOutputColor: #70e070;
|
|
77
92
|
qproperty-portConnectedColor: #ffd166;
|
|
78
|
-
|
|
93
|
+
|
|
79
94
|
qproperty-edgeColor: #c0c0c0;
|
|
80
95
|
qproperty-edgeSelectedColor: #ff8a5c;
|
|
81
96
|
}
|
|
82
97
|
""")
|
|
83
|
-
|
|
84
|
-
# Demo: add a couple of nodes
|
|
85
|
-
#editor.add_node("Value/Float", QPointF(100, 120))
|
|
86
|
-
#editor.add_node("Value/Float", QPointF(100, 240))
|
|
87
|
-
#editor.add_node("Math/Add", QPointF(420, 180))
|
|
88
98
|
editor.on_clear = self.tool.clear
|
|
89
99
|
|
|
90
100
|
u.editor[id] = editor
|
|
@@ -92,7 +102,7 @@ class Builder:
|
|
|
92
102
|
layout = QVBoxLayout()
|
|
93
103
|
layout.setMenuBar(self.setup_menu(dlg))
|
|
94
104
|
|
|
95
|
-
agents_list = AgentsWidget(self.window, tool=self.tool)
|
|
105
|
+
agents_list = AgentsWidget(self.window, tool=self.tool, parent=dlg)
|
|
96
106
|
list_widget = agents_list.setup()
|
|
97
107
|
list_widget.setFixedWidth(250)
|
|
98
108
|
center_splitter = QSplitter(Qt.Horizontal)
|
|
@@ -101,14 +111,30 @@ class Builder:
|
|
|
101
111
|
center_splitter.setStretchFactor(0, 1)
|
|
102
112
|
center_splitter.setStretchFactor(1, 8)
|
|
103
113
|
layout.addWidget(center_splitter)
|
|
104
|
-
|
|
114
|
+
# Make the splitter take all extra vertical space so the legend stays compact at the bottom
|
|
115
|
+
layout.setStretch(0, 1)
|
|
116
|
+
|
|
117
|
+
# Bottom legend as a compact, centered help label
|
|
118
|
+
legend_label = HelpLabel(
|
|
119
|
+
"Right-click: add node / undo / redo • Middle-click: pan view • Ctrl + Mouse wheel: zoom • "
|
|
120
|
+
"Left-click a port: create connection • Ctrl + Left-click a port: rewire or detach • Right-click or DEL a node/connection: remove"
|
|
121
|
+
)
|
|
122
|
+
legend_label.setAlignment(Qt.AlignCenter)
|
|
123
|
+
legend_label.setWordWrap(True)
|
|
124
|
+
legend_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
|
125
|
+
legend_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)
|
|
126
|
+
|
|
127
|
+
layout.addWidget(legend_label)
|
|
128
|
+
|
|
129
|
+
u.nodes["agent.builder.splitter"] = center_splitter
|
|
105
130
|
u.nodes["agent.builder.list"] = agents_list
|
|
131
|
+
u.nodes["agent.builder.legend"] = legend_label
|
|
106
132
|
|
|
107
133
|
dlg.setLayout(layout)
|
|
108
134
|
dlg.setWindowTitle(trans("agent.builder.title"))
|
|
109
135
|
|
|
110
136
|
def clear(self):
|
|
111
|
-
"""Clear
|
|
137
|
+
"""Clear dialog"""
|
|
112
138
|
pass
|
|
113
139
|
|
|
114
140
|
|
|
@@ -123,6 +149,7 @@ class BuilderDialog(BaseDialog):
|
|
|
123
149
|
super(BuilderDialog, self).__init__(window, id)
|
|
124
150
|
self.window = window
|
|
125
151
|
self.id = id # id
|
|
152
|
+
self._cleaned = False
|
|
126
153
|
|
|
127
154
|
def closeEvent(self, event):
|
|
128
155
|
"""
|
|
@@ -140,13 +167,81 @@ class BuilderDialog(BaseDialog):
|
|
|
140
167
|
:param event: key press event
|
|
141
168
|
"""
|
|
142
169
|
if event.key() == Qt.Key_Escape:
|
|
143
|
-
self.cleanup()
|
|
144
170
|
self.close()
|
|
145
171
|
else:
|
|
146
172
|
super(BuilderDialog, self).keyPressEvent(event)
|
|
147
173
|
|
|
148
174
|
def cleanup(self):
|
|
149
|
-
"""
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
self.
|
|
175
|
+
"""Cleanup on dialog close"""
|
|
176
|
+
if self._cleaned:
|
|
177
|
+
return
|
|
178
|
+
self._cleaned = True
|
|
179
|
+
|
|
180
|
+
tool = self.window.tools.get("agent_builder")
|
|
181
|
+
if tool:
|
|
182
|
+
try:
|
|
183
|
+
tool.on_close() # store current state (layout, nodes positions, etc.)
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
u = self.window.ui
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
u.nodes["agent.builder.list"].cleanup()
|
|
191
|
+
except Exception as e:
|
|
192
|
+
print("Agents list close failed:", e)
|
|
193
|
+
|
|
194
|
+
splitter = u.nodes.get("agent.builder.splitter")
|
|
195
|
+
editor = u.editor.pop('agent.builder', None)
|
|
196
|
+
|
|
197
|
+
# Detach editor from splitter before closing/deleting it to avoid dangling pointers in QSplitter
|
|
198
|
+
try:
|
|
199
|
+
if splitter is not None and editor is not None:
|
|
200
|
+
# Ensure the editor actually belongs to this splitter
|
|
201
|
+
idx = splitter.indexOf(editor) if editor.parent() is splitter else -1
|
|
202
|
+
if idx != -1:
|
|
203
|
+
# Create the replacement without a parent; QSplitter will adopt it.
|
|
204
|
+
from PySide6.QtWidgets import QWidget as _QW
|
|
205
|
+
placeholder = _QW()
|
|
206
|
+
splitter.replaceWidget(idx, placeholder)
|
|
207
|
+
except Exception as e:
|
|
208
|
+
print("Splitter detach failed:", e)
|
|
209
|
+
|
|
210
|
+
if editor is not None:
|
|
211
|
+
try:
|
|
212
|
+
editor.setStyleSheet("")
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
|
215
|
+
try:
|
|
216
|
+
editor.close()
|
|
217
|
+
except Exception as e:
|
|
218
|
+
print("NodeEditor close failed:", e)
|
|
219
|
+
try:
|
|
220
|
+
editor.setParent(None)
|
|
221
|
+
editor.deleteLater()
|
|
222
|
+
except Exception:
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
# Dispose legend label safely
|
|
226
|
+
legend = u.nodes.pop("agent.builder.legend", None)
|
|
227
|
+
if legend is not None:
|
|
228
|
+
try:
|
|
229
|
+
legend.setParent(None)
|
|
230
|
+
legend.deleteLater()
|
|
231
|
+
except Exception:
|
|
232
|
+
pass
|
|
233
|
+
|
|
234
|
+
# Drop splitter reference
|
|
235
|
+
try:
|
|
236
|
+
u.nodes.pop("agent.builder.splitter", None)
|
|
237
|
+
except Exception:
|
|
238
|
+
pass
|
|
239
|
+
|
|
240
|
+
# Ensure all deferred deletions are processed (do it a few rounds to be safe)
|
|
241
|
+
try:
|
|
242
|
+
from PySide6.QtWidgets import QApplication
|
|
243
|
+
for _ in range(3):
|
|
244
|
+
QApplication.sendPostedEvents(None, QEvent.DeferredDelete)
|
|
245
|
+
QApplication.processEvents()
|
|
246
|
+
except Exception:
|
|
247
|
+
pass
|