pygpt-net 2.6.61__py3-none-any.whl → 2.6.62__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 +7 -0
- pygpt_net/__init__.py +1 -1
- pygpt_net/controller/chat/response.py +8 -2
- pygpt_net/controller/settings/profile.py +16 -4
- pygpt_net/controller/settings/workdir.py +30 -5
- pygpt_net/controller/theme/common.py +4 -2
- pygpt_net/controller/theme/markdown.py +2 -2
- pygpt_net/controller/theme/theme.py +2 -1
- pygpt_net/controller/ui/ui.py +31 -3
- pygpt_net/core/agents/custom/llama_index/runner.py +18 -3
- pygpt_net/core/agents/custom/runner.py +10 -5
- pygpt_net/core/agents/runners/llama_workflow.py +65 -5
- pygpt_net/core/agents/runners/openai_workflow.py +2 -1
- pygpt_net/core/node_editor/types.py +13 -1
- pygpt_net/core/render/web/renderer.py +76 -11
- pygpt_net/data/config/config.json +2 -2
- pygpt_net/data/config/models.json +2 -2
- pygpt_net/data/css/style.dark.css +18 -0
- pygpt_net/data/css/style.light.css +20 -1
- pygpt_net/data/locale/locale.de.ini +2 -0
- pygpt_net/data/locale/locale.en.ini +2 -0
- pygpt_net/data/locale/locale.es.ini +2 -0
- pygpt_net/data/locale/locale.fr.ini +2 -0
- pygpt_net/data/locale/locale.it.ini +2 -0
- pygpt_net/data/locale/locale.pl.ini +3 -1
- pygpt_net/data/locale/locale.uk.ini +2 -0
- pygpt_net/data/locale/locale.zh.ini +2 -0
- pygpt_net/item/ctx.py +23 -1
- pygpt_net/provider/agents/llama_index/workflow/codeact.py +9 -6
- pygpt_net/provider/agents/llama_index/workflow/openai.py +38 -11
- pygpt_net/provider/agents/llama_index/workflow/planner.py +36 -16
- pygpt_net/provider/agents/llama_index/workflow/supervisor.py +60 -10
- pygpt_net/provider/agents/openai/agent.py +3 -1
- pygpt_net/provider/agents/openai/agent_b2b.py +13 -9
- pygpt_net/provider/agents/openai/agent_planner.py +6 -2
- pygpt_net/provider/agents/openai/agent_with_experts.py +4 -1
- pygpt_net/provider/agents/openai/agent_with_experts_feedback.py +4 -2
- pygpt_net/provider/agents/openai/agent_with_feedback.py +4 -2
- pygpt_net/provider/agents/openai/evolve.py +6 -2
- pygpt_net/provider/agents/openai/supervisor.py +3 -1
- pygpt_net/provider/api/openai/agents/response.py +1 -0
- pygpt_net/provider/core/config/patch.py +8 -0
- pygpt_net/tools/agent_builder/tool.py +6 -0
- pygpt_net/tools/agent_builder/ui/dialogs.py +0 -41
- pygpt_net/ui/layout/toolbox/presets.py +14 -2
- pygpt_net/ui/main.py +2 -2
- pygpt_net/ui/widget/dialog/confirm.py +27 -3
- pygpt_net/ui/widget/draw/painter.py +90 -1
- pygpt_net/ui/widget/lists/preset.py +289 -25
- pygpt_net/ui/widget/node_editor/editor.py +53 -15
- pygpt_net/ui/widget/node_editor/node.py +82 -104
- pygpt_net/ui/widget/node_editor/view.py +4 -5
- pygpt_net/ui/widget/textarea/input.py +155 -21
- {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.62.dist-info}/METADATA +17 -8
- {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.62.dist-info}/RECORD +58 -58
- {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.62.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.62.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.62.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.09.
|
|
9
|
+
# Updated Date: 2025.09.26 10:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
@@ -44,6 +44,9 @@ class SingleLineTextEdit(QTextEdit):
|
|
|
44
44
|
self.setTabChangesFocus(True)
|
|
45
45
|
self.setFrameStyle(QFrame.NoFrame)
|
|
46
46
|
self.document().setDocumentMargin(2)
|
|
47
|
+
# Enforce line-edit-like behavior with no context menu
|
|
48
|
+
# (widget menus are disabled by design; only node menu remains)
|
|
49
|
+
self.setContextMenuPolicy(Qt.NoContextMenu)
|
|
47
50
|
self._update_height()
|
|
48
51
|
|
|
49
52
|
def _update_height(self):
|
|
@@ -86,58 +89,13 @@ class SingleLineTextEdit(QTextEdit):
|
|
|
86
89
|
c.setPosition(min(pos, len(t2)))
|
|
87
90
|
self.setTextCursor(c)
|
|
88
91
|
|
|
89
|
-
def _apply_menu_theme(self, menu: QMenu):
|
|
90
|
-
"""Apply app/window stylesheet + palette + font to context menu."""
|
|
91
|
-
try:
|
|
92
|
-
wnd = self.window()
|
|
93
|
-
except Exception:
|
|
94
|
-
wnd = None
|
|
95
|
-
stylesheet = ""
|
|
96
|
-
pal = None
|
|
97
|
-
font = None
|
|
98
|
-
try:
|
|
99
|
-
if wnd:
|
|
100
|
-
stylesheet = wnd.styleSheet() or ""
|
|
101
|
-
pal = wnd.palette()
|
|
102
|
-
font = wnd.font()
|
|
103
|
-
except Exception:
|
|
104
|
-
pass
|
|
105
|
-
try:
|
|
106
|
-
app = QApplication.instance()
|
|
107
|
-
if app:
|
|
108
|
-
if not stylesheet and app.styleSheet():
|
|
109
|
-
stylesheet = app.styleSheet()
|
|
110
|
-
if pal is None:
|
|
111
|
-
pal = app.palette()
|
|
112
|
-
if font is None:
|
|
113
|
-
font = app.font()
|
|
114
|
-
except Exception:
|
|
115
|
-
pass
|
|
116
|
-
try:
|
|
117
|
-
if pal:
|
|
118
|
-
menu.setPalette(pal)
|
|
119
|
-
if font:
|
|
120
|
-
menu.setFont(font)
|
|
121
|
-
if stylesheet:
|
|
122
|
-
menu.setStyleSheet(stylesheet)
|
|
123
|
-
menu.ensurePolished()
|
|
124
|
-
except Exception:
|
|
125
|
-
pass
|
|
126
|
-
|
|
127
92
|
def contextMenuEvent(self, e):
|
|
128
|
-
"""
|
|
93
|
+
"""Widget-level context menu is intentionally disabled."""
|
|
129
94
|
try:
|
|
130
|
-
|
|
95
|
+
e.ignore()
|
|
131
96
|
except Exception:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
try:
|
|
135
|
-
menu.exec(e.globalPos())
|
|
136
|
-
finally:
|
|
137
|
-
try:
|
|
138
|
-
menu.deleteLater()
|
|
139
|
-
except Exception:
|
|
140
|
-
pass
|
|
97
|
+
pass
|
|
98
|
+
return
|
|
141
99
|
|
|
142
100
|
|
|
143
101
|
class NodeContentWidget(QWidget):
|
|
@@ -229,6 +187,8 @@ class NodeContentWidget(QWidget):
|
|
|
229
187
|
elif pm.type == "text":
|
|
230
188
|
te = QTextEdit()
|
|
231
189
|
te.setFocusPolicy(Qt.StrongFocus)
|
|
190
|
+
# Disable widget-level context menu completely (only node menu is available)
|
|
191
|
+
te.setContextMenuPolicy(Qt.NoContextMenu)
|
|
232
192
|
if pm.value is not None:
|
|
233
193
|
te.setPlainText(str(pm.value))
|
|
234
194
|
te.setReadOnly(not editable)
|
|
@@ -238,8 +198,6 @@ class NodeContentWidget(QWidget):
|
|
|
238
198
|
except Exception:
|
|
239
199
|
pass
|
|
240
200
|
te.textChanged.connect(lambda pid=pid, te=te: self.valueChanged.emit(pid, te.toPlainText()))
|
|
241
|
-
# Ensure context menu follows global (Material) style
|
|
242
|
-
self._install_styled_context_menu(te)
|
|
243
201
|
w = te
|
|
244
202
|
|
|
245
203
|
elif pm.type == "int":
|
|
@@ -314,57 +272,6 @@ class NodeContentWidget(QWidget):
|
|
|
314
272
|
except Exception:
|
|
315
273
|
pass
|
|
316
274
|
|
|
317
|
-
def _install_styled_context_menu(self, te: QTextEdit):
|
|
318
|
-
"""Install a custom context menu handler that applies global styles."""
|
|
319
|
-
try:
|
|
320
|
-
te.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
321
|
-
te.customContextMenuRequested.connect(
|
|
322
|
-
lambda pos, _te=te: self._show_styled_standard_menu(_te, pos)
|
|
323
|
-
)
|
|
324
|
-
except Exception:
|
|
325
|
-
pass
|
|
326
|
-
|
|
327
|
-
def _show_styled_standard_menu(self, te: QTextEdit, pos):
|
|
328
|
-
"""Create standard menu and apply app/window stylesheet + palette + font."""
|
|
329
|
-
try:
|
|
330
|
-
menu = te.createStandardContextMenu()
|
|
331
|
-
except Exception:
|
|
332
|
-
return
|
|
333
|
-
stylesheet = ""
|
|
334
|
-
pal = None
|
|
335
|
-
font = None
|
|
336
|
-
try:
|
|
337
|
-
# Prefer editor helpers (consistent with the rest of NodeEditor)
|
|
338
|
-
stylesheet = self.editor._current_stylesheet()
|
|
339
|
-
pal = self.editor._current_palette()
|
|
340
|
-
font = self.editor._current_font()
|
|
341
|
-
except Exception:
|
|
342
|
-
try:
|
|
343
|
-
wnd = te.window()
|
|
344
|
-
if wnd:
|
|
345
|
-
stylesheet = wnd.styleSheet() or ""
|
|
346
|
-
pal = wnd.palette()
|
|
347
|
-
font = wnd.font()
|
|
348
|
-
except Exception:
|
|
349
|
-
pass
|
|
350
|
-
try:
|
|
351
|
-
if pal:
|
|
352
|
-
menu.setPalette(pal)
|
|
353
|
-
if font:
|
|
354
|
-
menu.setFont(font)
|
|
355
|
-
if stylesheet:
|
|
356
|
-
menu.setStyleSheet(stylesheet)
|
|
357
|
-
menu.ensurePolished()
|
|
358
|
-
except Exception:
|
|
359
|
-
pass
|
|
360
|
-
try:
|
|
361
|
-
menu.exec(te.mapToGlobal(pos))
|
|
362
|
-
finally:
|
|
363
|
-
try:
|
|
364
|
-
menu.deleteLater()
|
|
365
|
-
except Exception:
|
|
366
|
-
pass
|
|
367
|
-
|
|
368
275
|
def event(self, e):
|
|
369
276
|
"""
|
|
370
277
|
Filter ShortcutOverride so editor keystrokes are not eaten by the scene.
|
|
@@ -825,7 +732,7 @@ class NodeItem(QGraphicsWidget):
|
|
|
825
732
|
def _effective_hit_margin(self) -> float:
|
|
826
733
|
"""Return the effective resize-grip 'hit' margin (visual margin minus inset)."""
|
|
827
734
|
margin = float(getattr(self.editor, "_resize_grip_margin", 12.0) or 12.0)
|
|
828
|
-
inset = float(getattr(self.editor, "_resize_grip_hit_inset",
|
|
735
|
+
inset = float(getattr(self.editor, "_resize_grip_hit_inset", 5.0) or 0.0)
|
|
829
736
|
hit = max(4.0, margin - inset)
|
|
830
737
|
return hit
|
|
831
738
|
|
|
@@ -1216,6 +1123,77 @@ class NodeItem(QGraphicsWidget):
|
|
|
1216
1123
|
"""Filter events on proxy/content to keep hover/ports/overlay in sync."""
|
|
1217
1124
|
et = e.type()
|
|
1218
1125
|
try:
|
|
1126
|
+
# Forward RMB on the proxy (covers all embedded editors) to node menu
|
|
1127
|
+
if obj is self._proxy:
|
|
1128
|
+
if et == QEvent.GraphicsSceneMousePress:
|
|
1129
|
+
try:
|
|
1130
|
+
if e.button() == Qt.RightButton:
|
|
1131
|
+
# Use screenPos if available; fallback to view mapping
|
|
1132
|
+
gp = e.screenPos() if hasattr(e, "screenPos") else None
|
|
1133
|
+
if gp is None:
|
|
1134
|
+
sp = e.scenePos()
|
|
1135
|
+
vp = self.editor.view.mapFromScene(sp)
|
|
1136
|
+
gp = self.editor.view.viewport().mapToGlobal(vp)
|
|
1137
|
+
# Reuse node menu logic here for consistency
|
|
1138
|
+
menu = QMenu(self.editor.window())
|
|
1139
|
+
ss = self.editor.window().styleSheet()
|
|
1140
|
+
if ss:
|
|
1141
|
+
menu.setStyleSheet(ss)
|
|
1142
|
+
act_rename = QAction(self.editor.config.node_context_rename(), menu)
|
|
1143
|
+
act_delete = QAction(self.editor.config.node_context_delete(), menu)
|
|
1144
|
+
menu.addAction(act_rename)
|
|
1145
|
+
menu.addSeparator()
|
|
1146
|
+
menu.addAction(act_delete)
|
|
1147
|
+
chosen = menu.exec(gp.toPoint() if hasattr(gp, "toPoint") else gp)
|
|
1148
|
+
if chosen == act_rename:
|
|
1149
|
+
from PySide6.QtWidgets import QInputDialog
|
|
1150
|
+
new_name, ok = QInputDialog.getText(
|
|
1151
|
+
self.editor.window(),
|
|
1152
|
+
self.editor.config.rename_dialog_title(),
|
|
1153
|
+
self.editor.config.rename_dialog_label(),
|
|
1154
|
+
text=self.node.name
|
|
1155
|
+
)
|
|
1156
|
+
if ok and new_name:
|
|
1157
|
+
self.node.name = new_name
|
|
1158
|
+
self.update()
|
|
1159
|
+
elif chosen == act_delete:
|
|
1160
|
+
self.editor._delete_node_item(self)
|
|
1161
|
+
e.accept()
|
|
1162
|
+
return True
|
|
1163
|
+
except Exception:
|
|
1164
|
+
pass
|
|
1165
|
+
|
|
1166
|
+
if et == QEvent.GraphicsSceneContextMenu:
|
|
1167
|
+
try:
|
|
1168
|
+
gp = e.screenPos() if hasattr(e, "screenPos") else None
|
|
1169
|
+
menu = QMenu(self.editor.window())
|
|
1170
|
+
ss = self.editor.window().styleSheet()
|
|
1171
|
+
if ss:
|
|
1172
|
+
menu.setStyleSheet(ss)
|
|
1173
|
+
act_rename = QAction(self.editor.config.node_context_rename(), menu)
|
|
1174
|
+
act_delete = QAction(self.editor.config.node_context_delete(), menu)
|
|
1175
|
+
menu.addAction(act_rename)
|
|
1176
|
+
menu.addSeparator()
|
|
1177
|
+
menu.addAction(act_delete)
|
|
1178
|
+
chosen = menu.exec(gp)
|
|
1179
|
+
if chosen == act_rename:
|
|
1180
|
+
from PySide6.QtWidgets import QInputDialog
|
|
1181
|
+
new_name, ok = QInputDialog.getText(
|
|
1182
|
+
self.editor.window(),
|
|
1183
|
+
self.editor.config.rename_dialog_title(),
|
|
1184
|
+
self.editor.config.rename_dialog_label(),
|
|
1185
|
+
text=self.node.name
|
|
1186
|
+
)
|
|
1187
|
+
if ok and new_name:
|
|
1188
|
+
self.node.name = new_name
|
|
1189
|
+
self.update()
|
|
1190
|
+
elif chosen == act_delete:
|
|
1191
|
+
self.editor._delete_node_item(self)
|
|
1192
|
+
e.accept()
|
|
1193
|
+
return True
|
|
1194
|
+
except Exception:
|
|
1195
|
+
pass
|
|
1196
|
+
|
|
1219
1197
|
if obj is self._proxy and et in (QEvent.GraphicsSceneMouseMove, QEvent.GraphicsSceneHoverMove):
|
|
1220
1198
|
local = self.mapFromScene(e.scenePos())
|
|
1221
1199
|
self._apply_hover_from_pos(local)
|
|
@@ -254,7 +254,6 @@ class NodeViewOverlayControls(QWidget):
|
|
|
254
254
|
self.setAttribute(Qt.WA_StyledBackground, True)
|
|
255
255
|
|
|
256
256
|
layout = QHBoxLayout(self)
|
|
257
|
-
# Bigger spacing to visually add padding around buttons
|
|
258
257
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
259
258
|
layout.setSpacing(8)
|
|
260
259
|
|
|
@@ -266,21 +265,21 @@ class NodeViewOverlayControls(QWidget):
|
|
|
266
265
|
self.btnGrab.setToolTip(cfg.overlay_grab_tooltip())
|
|
267
266
|
self.btnGrab.setIcon(QIcon(":/icons/drag.svg"))
|
|
268
267
|
self.btnGrab.setIconSize(QSize(20, 20))
|
|
269
|
-
self.btnGrab.setMinimumSize(
|
|
268
|
+
self.btnGrab.setMinimumSize(25, 25)
|
|
270
269
|
|
|
271
|
-
# Zoom Out
|
|
270
|
+
# Zoom Out
|
|
272
271
|
self.btnZoomOut = QPushButton(self)
|
|
273
272
|
self.btnZoomOut.setToolTip(cfg.overlay_zoom_out_tooltip())
|
|
274
273
|
self.btnZoomOut.setIcon(QIcon(":/icons/zoom_out.svg"))
|
|
275
274
|
self.btnZoomOut.setIconSize(QSize(20, 20))
|
|
276
|
-
self.btnZoomOut.setMinimumSize(
|
|
275
|
+
self.btnZoomOut.setMinimumSize(25, 25)
|
|
277
276
|
|
|
278
277
|
# Zoom In
|
|
279
278
|
self.btnZoomIn = QPushButton(self)
|
|
280
279
|
self.btnZoomIn.setToolTip(cfg.overlay_zoom_in_tooltip())
|
|
281
280
|
self.btnZoomIn.setIcon(QIcon(":/icons/zoom_in.svg"))
|
|
282
281
|
self.btnZoomIn.setIconSize(QSize(20, 20))
|
|
283
|
-
self.btnZoomIn.setMinimumSize(
|
|
282
|
+
self.btnZoomIn.setMinimumSize(25, 25)
|
|
284
283
|
|
|
285
284
|
layout.addWidget(self.btnGrab)
|
|
286
285
|
layout.addWidget(self.btnZoomIn)
|
|
@@ -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.
|
|
9
|
+
# Updated Date: 2025.09.26 12:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from typing import Optional
|
|
@@ -62,8 +62,8 @@ class ChatInput(QTextEdit):
|
|
|
62
62
|
self._icons_margin = 6 # inner left/right padding around the bar
|
|
63
63
|
self._icons_spacing = 4 # spacing between buttons
|
|
64
64
|
self._icons_offset_y = -4 # small upward shift (visual alignment)
|
|
65
|
-
self._icon_size = QSize(18, 18) # icon size (matches
|
|
66
|
-
self._btn_size = QSize(24, 24) # button size (w x h), matches
|
|
65
|
+
self._icon_size = QSize(18, 18) # icon size (matches original)
|
|
66
|
+
self._btn_size = QSize(24, 24) # button size (w x h), matches QPushButton
|
|
67
67
|
|
|
68
68
|
# Storage for icon buttons and metadata
|
|
69
69
|
self._icons = {} # key -> QPushButton
|
|
@@ -124,6 +124,9 @@ class ChatInput(QTextEdit):
|
|
|
124
124
|
self._tokens_timer.timeout.connect(self.window.controller.ui.update_tokens)
|
|
125
125
|
self.textChanged.connect(self._on_text_changed_tokens)
|
|
126
126
|
|
|
127
|
+
# Paste/input safety limits
|
|
128
|
+
self._paste_max_chars = 100000000 # hard cap to prevent pathological pastes from freezing/crashing
|
|
129
|
+
|
|
127
130
|
def _on_text_changed_tokens(self):
|
|
128
131
|
"""Schedule token count update with debounce."""
|
|
129
132
|
self._tokens_timer.start()
|
|
@@ -142,15 +145,133 @@ class ChatInput(QTextEdit):
|
|
|
142
145
|
self._text_top_padding = max(0, int(px))
|
|
143
146
|
self._apply_margins()
|
|
144
147
|
|
|
148
|
+
def canInsertFromMimeData(self, source) -> bool:
|
|
149
|
+
"""
|
|
150
|
+
Restrict accepted MIME types to safe, explicitly handled ones.
|
|
151
|
+
This prevents Qt from trying to parse unknown/broken formats.
|
|
152
|
+
"""
|
|
153
|
+
try:
|
|
154
|
+
if source is None:
|
|
155
|
+
return False
|
|
156
|
+
return source.hasText() or source.hasUrls() or source.hasImage()
|
|
157
|
+
except Exception:
|
|
158
|
+
return False
|
|
159
|
+
|
|
145
160
|
def insertFromMimeData(self, source):
|
|
146
161
|
"""
|
|
147
162
|
Insert from mime data
|
|
148
163
|
|
|
149
164
|
:param source: source
|
|
150
165
|
"""
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
166
|
+
# Always process attachments first; never break input pipeline on errors.
|
|
167
|
+
try:
|
|
168
|
+
self.handle_clipboard(source)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
try:
|
|
171
|
+
self.window.core.debug.log(e)
|
|
172
|
+
except Exception:
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
# If an image is present, we treat it as attachment-only and do not insert textual representation.
|
|
176
|
+
try:
|
|
177
|
+
if source and source.hasImage():
|
|
178
|
+
return
|
|
179
|
+
except Exception:
|
|
180
|
+
# fallback to text extraction below
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
# Insert only sanitized plain text (no HTML, no custom formats).
|
|
184
|
+
try:
|
|
185
|
+
text = self._safe_text_from_mime(source)
|
|
186
|
+
if text:
|
|
187
|
+
self.insertPlainText(text)
|
|
188
|
+
except Exception as e:
|
|
189
|
+
try:
|
|
190
|
+
self.window.core.debug.log(e)
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
def _safe_text_from_mime(self, source) -> str:
|
|
195
|
+
"""
|
|
196
|
+
Extracts plain text from QMimeData safely, normalizes and sanitizes it.
|
|
197
|
+
Falls back to URLs joined by space if textual content is not provided.
|
|
198
|
+
"""
|
|
199
|
+
try:
|
|
200
|
+
if source is None:
|
|
201
|
+
return ""
|
|
202
|
+
if source.hasText():
|
|
203
|
+
return self._sanitize_text(source.text())
|
|
204
|
+
if source.hasUrls():
|
|
205
|
+
parts = []
|
|
206
|
+
for url in source.urls():
|
|
207
|
+
try:
|
|
208
|
+
if url.isLocalFile():
|
|
209
|
+
parts.append(url.toLocalFile())
|
|
210
|
+
else:
|
|
211
|
+
parts.append(url.toString())
|
|
212
|
+
except Exception:
|
|
213
|
+
continue
|
|
214
|
+
return self._sanitize_text(" ".join([p for p in parts if p]))
|
|
215
|
+
except Exception as e:
|
|
216
|
+
try:
|
|
217
|
+
self.window.core.debug.log(e)
|
|
218
|
+
except Exception:
|
|
219
|
+
pass
|
|
220
|
+
return ""
|
|
221
|
+
|
|
222
|
+
def _sanitize_text(self, text: str) -> str:
|
|
223
|
+
"""
|
|
224
|
+
Sanitize pasted text:
|
|
225
|
+
- normalize newlines
|
|
226
|
+
- remove NUL and most control chars except tab/newline
|
|
227
|
+
- strip zero-width and bidi control characters
|
|
228
|
+
- hard-cap maximum length to avoid UI freeze
|
|
229
|
+
"""
|
|
230
|
+
if not text:
|
|
231
|
+
return ""
|
|
232
|
+
if not isinstance(text, str):
|
|
233
|
+
try:
|
|
234
|
+
text = str(text)
|
|
235
|
+
except Exception:
|
|
236
|
+
return ""
|
|
237
|
+
|
|
238
|
+
# Normalize line breaks
|
|
239
|
+
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
|
240
|
+
|
|
241
|
+
# Remove disallowed control chars, keep tab/newline
|
|
242
|
+
out = []
|
|
243
|
+
for ch in text:
|
|
244
|
+
code = ord(ch)
|
|
245
|
+
if code == 0:
|
|
246
|
+
continue # NUL
|
|
247
|
+
if code < 32:
|
|
248
|
+
if ch in ("\n", "\t"):
|
|
249
|
+
out.append(ch)
|
|
250
|
+
else:
|
|
251
|
+
out.append(" ")
|
|
252
|
+
continue
|
|
253
|
+
if code == 0x7F:
|
|
254
|
+
continue # DEL
|
|
255
|
+
# Remove zero-width and bidi controls
|
|
256
|
+
if (0x200B <= code <= 0x200F) or (0x202A <= code <= 0x202E) or (0x2066 <= code <= 0x2069):
|
|
257
|
+
continue
|
|
258
|
+
out.append(ch)
|
|
259
|
+
|
|
260
|
+
s = "".join(out)
|
|
261
|
+
|
|
262
|
+
# Cap very large pastes
|
|
263
|
+
try:
|
|
264
|
+
limit = int(self._paste_max_chars)
|
|
265
|
+
except Exception:
|
|
266
|
+
limit = 250000
|
|
267
|
+
if limit > 0 and len(s) > limit:
|
|
268
|
+
s = s[:limit]
|
|
269
|
+
try:
|
|
270
|
+
self.window.core.debug.log(f"Input paste truncated to {limit} chars")
|
|
271
|
+
except Exception:
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
return s
|
|
154
275
|
|
|
155
276
|
def handle_clipboard(self, source):
|
|
156
277
|
"""
|
|
@@ -158,20 +279,33 @@ class ChatInput(QTextEdit):
|
|
|
158
279
|
|
|
159
280
|
:param source: source
|
|
160
281
|
"""
|
|
161
|
-
if source
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
282
|
+
if source is None:
|
|
283
|
+
return
|
|
284
|
+
try:
|
|
285
|
+
if source.hasImage():
|
|
286
|
+
image = source.imageData()
|
|
287
|
+
if isinstance(image, QImage):
|
|
288
|
+
self.window.controller.attachment.from_clipboard_image(image)
|
|
289
|
+
elif source.hasUrls():
|
|
290
|
+
urls = source.urls()
|
|
291
|
+
for url in urls:
|
|
292
|
+
try:
|
|
293
|
+
if url.isLocalFile():
|
|
294
|
+
local_path = url.toLocalFile()
|
|
295
|
+
self.window.controller.attachment.from_clipboard_url(local_path)
|
|
296
|
+
except Exception:
|
|
297
|
+
# Ignore broken URL entries
|
|
298
|
+
continue
|
|
299
|
+
elif source.hasText():
|
|
300
|
+
text = self._sanitize_text(source.text())
|
|
301
|
+
if text:
|
|
302
|
+
self.window.controller.attachment.from_clipboard_text(text)
|
|
303
|
+
except Exception as e:
|
|
304
|
+
# Never propagate clipboard errors to UI thread
|
|
305
|
+
try:
|
|
306
|
+
self.window.core.debug.log(e)
|
|
307
|
+
except Exception:
|
|
308
|
+
pass
|
|
175
309
|
|
|
176
310
|
def contextMenuEvent(self, event):
|
|
177
311
|
"""
|
|
@@ -395,7 +529,7 @@ class ChatInput(QTextEdit):
|
|
|
395
529
|
btn.setCursor(Qt.PointingHandCursor)
|
|
396
530
|
btn.setToolTip(tooltip or key)
|
|
397
531
|
btn.setFocusPolicy(Qt.NoFocus)
|
|
398
|
-
btn.setFlat(True) # flat button style
|
|
532
|
+
btn.setFlat(True) # flat button style
|
|
399
533
|
# optional: no text
|
|
400
534
|
btn.setText("")
|
|
401
535
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pygpt-net
|
|
3
|
-
Version: 2.6.
|
|
3
|
+
Version: 2.6.62
|
|
4
4
|
Summary: Desktop AI Assistant powered by: OpenAI GPT-5, GPT-4, o1, o3, Gemini, Claude, Grok, DeepSeek, and other models supported by Llama Index, and Ollama. Chatbot, agents, completion, image generation, vision analysis, speech-to-text, plugins, MCP, internet access, file handling, command execution and more.
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: ai,api,api key,app,assistant,bielik,chat,chatbot,chatgpt,claude,dall-e,deepseek,desktop,gemini,gpt,gpt-3.5,gpt-4,gpt-4-vision,gpt-4o,gpt-5,gpt-oss,gpt3.5,gpt4,grok,langchain,llama-index,llama3,mistral,o1,o3,ollama,openai,presets,py-gpt,py_gpt,pygpt,pyside,qt,text completion,tts,ui,vision,whisper
|
|
@@ -117,7 +117,7 @@ Description-Content-Type: text/markdown
|
|
|
117
117
|
|
|
118
118
|
[](https://snapcraft.io/pygpt)
|
|
119
119
|
|
|
120
|
-
Release: **2.6.
|
|
120
|
+
Release: **2.6.62** | build: **2025-09-26** | Python: **>=3.10, <3.14**
|
|
121
121
|
|
|
122
122
|
> Official website: https://pygpt.net | Documentation: https://pygpt.readthedocs.io
|
|
123
123
|
>
|
|
@@ -787,6 +787,8 @@ Includes built-in agents (Legacy):
|
|
|
787
787
|
|
|
788
788
|
In the future, the list of built-in agents will be expanded.
|
|
789
789
|
|
|
790
|
+
You can create your own types (workflows/patterns) using the built-in visual node-based editor found in the `Tools -> Agents Builder`.
|
|
791
|
+
|
|
790
792
|
You can also create your own agent by creating a new provider that inherits from `pygpt_net.provider.agents.base`.
|
|
791
793
|
|
|
792
794
|
**Tools and Plugins**
|
|
@@ -824,7 +826,7 @@ It allows running agents for OpenAI models and models compatible with the OpenAI
|
|
|
824
826
|
|
|
825
827
|
In this mode, you can use pre-configured Experts in Expert mode presets - they will be launched as agents (in the `openai_agents_experts` type, which allows launching one main agent and subordinate agents to which queries will be appropriately directed).
|
|
826
828
|
|
|
827
|
-
**Agent types:**
|
|
829
|
+
**Agent types (workflows/patterns):**
|
|
828
830
|
|
|
829
831
|
- `Agent with experts` - uses attached experts as sub-agents
|
|
830
832
|
- `Agent with experts + feedback` - uses attached experts as sub-agents + feedback agent in a loop
|
|
@@ -836,7 +838,7 @@ In this mode, you can use pre-configured Experts in Expert mode presets - they w
|
|
|
836
838
|
- `B2B` - bot-to-bot communication, involving two bots interacting with each other while keeping a human in the loop.
|
|
837
839
|
- `Supervisor + Worker` - one agent (supervisor) acts as a bridge between the user and the second agent (worker). The user provides a query to the supervisor, who then sends instructions to the worker until the task is completed by the worker.
|
|
838
840
|
|
|
839
|
-
|
|
841
|
+
You can create your own types (workflows/patterns) using the built-in visual node-based editor found in the `Tools -> Agents Builder`.
|
|
840
842
|
|
|
841
843
|
There are also predefined presets added as examples:
|
|
842
844
|
|
|
@@ -2181,10 +2183,10 @@ To add a new element, right-click on the editor grid and select `Add` to insert
|
|
|
2181
2183
|
|
|
2182
2184
|
**Types of Nodes:**
|
|
2183
2185
|
|
|
2184
|
-
- **
|
|
2185
|
-
- **
|
|
2186
|
-
- **
|
|
2187
|
-
- **
|
|
2186
|
+
- **Start**: The starting point for agents (user input).
|
|
2187
|
+
- **Agent**: A single agent with customizable default parameters, such as system instructions and tool usage. These settings can be overridden in the preset.
|
|
2188
|
+
- **Memory**: Shared memory between agents (shared Context).
|
|
2189
|
+
- **End**: The endpoint, returning control to the user.
|
|
2188
2190
|
|
|
2189
2191
|
Agents with connected shared memory share it among themselves. Agents without shared memory only receive the latest output from the previous agent.
|
|
2190
2192
|
|
|
@@ -3721,6 +3723,13 @@ may consume additional tokens that are not displayed in the main window.
|
|
|
3721
3723
|
|
|
3722
3724
|
## Recent changes:
|
|
3723
3725
|
|
|
3726
|
+
**2.6.62 (2025-09-26)**
|
|
3727
|
+
|
|
3728
|
+
- Enhanced agent workflow execution.
|
|
3729
|
+
- Improved preset list handling by adding a drop field indicator and fixing auto-scroll.
|
|
3730
|
+
- Added middle-mouse button panning to Painter.
|
|
3731
|
+
- Added an input character counter.
|
|
3732
|
+
|
|
3724
3733
|
**2.6.61 (2025-09-26)**
|
|
3725
3734
|
|
|
3726
3735
|
- Enhanced the agents node editor, custom agent flow, and instruction following.
|