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.
Files changed (58) hide show
  1. pygpt_net/CHANGELOG.txt +7 -0
  2. pygpt_net/__init__.py +1 -1
  3. pygpt_net/controller/chat/response.py +8 -2
  4. pygpt_net/controller/settings/profile.py +16 -4
  5. pygpt_net/controller/settings/workdir.py +30 -5
  6. pygpt_net/controller/theme/common.py +4 -2
  7. pygpt_net/controller/theme/markdown.py +2 -2
  8. pygpt_net/controller/theme/theme.py +2 -1
  9. pygpt_net/controller/ui/ui.py +31 -3
  10. pygpt_net/core/agents/custom/llama_index/runner.py +18 -3
  11. pygpt_net/core/agents/custom/runner.py +10 -5
  12. pygpt_net/core/agents/runners/llama_workflow.py +65 -5
  13. pygpt_net/core/agents/runners/openai_workflow.py +2 -1
  14. pygpt_net/core/node_editor/types.py +13 -1
  15. pygpt_net/core/render/web/renderer.py +76 -11
  16. pygpt_net/data/config/config.json +2 -2
  17. pygpt_net/data/config/models.json +2 -2
  18. pygpt_net/data/css/style.dark.css +18 -0
  19. pygpt_net/data/css/style.light.css +20 -1
  20. pygpt_net/data/locale/locale.de.ini +2 -0
  21. pygpt_net/data/locale/locale.en.ini +2 -0
  22. pygpt_net/data/locale/locale.es.ini +2 -0
  23. pygpt_net/data/locale/locale.fr.ini +2 -0
  24. pygpt_net/data/locale/locale.it.ini +2 -0
  25. pygpt_net/data/locale/locale.pl.ini +3 -1
  26. pygpt_net/data/locale/locale.uk.ini +2 -0
  27. pygpt_net/data/locale/locale.zh.ini +2 -0
  28. pygpt_net/item/ctx.py +23 -1
  29. pygpt_net/provider/agents/llama_index/workflow/codeact.py +9 -6
  30. pygpt_net/provider/agents/llama_index/workflow/openai.py +38 -11
  31. pygpt_net/provider/agents/llama_index/workflow/planner.py +36 -16
  32. pygpt_net/provider/agents/llama_index/workflow/supervisor.py +60 -10
  33. pygpt_net/provider/agents/openai/agent.py +3 -1
  34. pygpt_net/provider/agents/openai/agent_b2b.py +13 -9
  35. pygpt_net/provider/agents/openai/agent_planner.py +6 -2
  36. pygpt_net/provider/agents/openai/agent_with_experts.py +4 -1
  37. pygpt_net/provider/agents/openai/agent_with_experts_feedback.py +4 -2
  38. pygpt_net/provider/agents/openai/agent_with_feedback.py +4 -2
  39. pygpt_net/provider/agents/openai/evolve.py +6 -2
  40. pygpt_net/provider/agents/openai/supervisor.py +3 -1
  41. pygpt_net/provider/api/openai/agents/response.py +1 -0
  42. pygpt_net/provider/core/config/patch.py +8 -0
  43. pygpt_net/tools/agent_builder/tool.py +6 -0
  44. pygpt_net/tools/agent_builder/ui/dialogs.py +0 -41
  45. pygpt_net/ui/layout/toolbox/presets.py +14 -2
  46. pygpt_net/ui/main.py +2 -2
  47. pygpt_net/ui/widget/dialog/confirm.py +27 -3
  48. pygpt_net/ui/widget/draw/painter.py +90 -1
  49. pygpt_net/ui/widget/lists/preset.py +289 -25
  50. pygpt_net/ui/widget/node_editor/editor.py +53 -15
  51. pygpt_net/ui/widget/node_editor/node.py +82 -104
  52. pygpt_net/ui/widget/node_editor/view.py +4 -5
  53. pygpt_net/ui/widget/textarea/input.py +155 -21
  54. {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.62.dist-info}/METADATA +17 -8
  55. {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.62.dist-info}/RECORD +58 -58
  56. {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.62.dist-info}/LICENSE +0 -0
  57. {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.62.dist-info}/WHEEL +0 -0
  58. {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.25 15:32:39 #
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
- """Ensure standard context menu follows app-wide (e.g., Qt Material) styling."""
93
+ """Widget-level context menu is intentionally disabled."""
129
94
  try:
130
- menu = self.createStandardContextMenu()
95
+ e.ignore()
131
96
  except Exception:
132
- return super().contextMenuEvent(e)
133
- self._apply_menu_theme(menu)
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", 3.0) or 0.0)
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(32, 32)
268
+ self.btnGrab.setMinimumSize(25, 25)
270
269
 
271
- # Zoom Out (placed before Zoom In)
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(32, 32)
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(32, 32)
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.22 12:00:00 #
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 your original)
66
- self._btn_size = QSize(24, 24) # button size (w x h), matches previous QPushButton
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
- self.handle_clipboard(source)
152
- if not source.hasImage():
153
- super().insertFromMimeData(source)
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.hasImage():
162
- image = source.imageData()
163
- if isinstance(image, QImage):
164
- self.window.controller.attachment.from_clipboard_image(image)
165
- elif source.hasUrls():
166
- urls = source.urls()
167
- for url in urls:
168
- if url.isLocalFile():
169
- local_path = url.toLocalFile()
170
- self.window.controller.attachment.from_clipboard_url(local_path)
171
- elif source.hasText():
172
- text = source.text()
173
- if text:
174
- self.window.controller.attachment.from_clipboard_text(text)
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 like your original
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.61
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
  [![pygpt](https://snapcraft.io/pygpt/badge.svg)](https://snapcraft.io/pygpt)
119
119
 
120
- Release: **2.6.61** | build: **2025-09-26** | Python: **>=3.10, <3.14**
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
- More types will be available in the future.
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
- - **Flow/Start**: The starting point for agents (user input).
2185
- - **Flow/Agent**: A single agent with customizable default parameters, such as system instructions and tool usage. These settings can be overridden in the preset.
2186
- - **Flow/Memory**: Shared memory between agents (shared Context).
2187
- - **Flow/End**: The endpoint, returning control to the user.
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.