pygpt-net 2.6.60__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 (60) hide show
  1. pygpt_net/CHANGELOG.txt +7 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/chat/common.py +115 -6
  4. pygpt_net/controller/chat/input.py +4 -1
  5. pygpt_net/controller/presets/presets.py +121 -6
  6. pygpt_net/controller/settings/editor.py +0 -15
  7. pygpt_net/controller/theme/markdown.py +2 -5
  8. pygpt_net/controller/ui/ui.py +4 -7
  9. pygpt_net/core/agents/custom/__init__.py +7 -1
  10. pygpt_net/core/agents/custom/llama_index/factory.py +17 -6
  11. pygpt_net/core/agents/custom/llama_index/runner.py +35 -2
  12. pygpt_net/core/agents/custom/llama_index/utils.py +12 -1
  13. pygpt_net/core/agents/custom/router.py +45 -6
  14. pygpt_net/core/agents/custom/runner.py +2 -1
  15. pygpt_net/core/agents/custom/schema.py +3 -1
  16. pygpt_net/core/agents/custom/utils.py +13 -1
  17. pygpt_net/core/db/viewer.py +11 -5
  18. pygpt_net/core/node_editor/graph.py +18 -9
  19. pygpt_net/core/node_editor/models.py +9 -2
  20. pygpt_net/core/node_editor/types.py +3 -1
  21. pygpt_net/core/presets/presets.py +216 -29
  22. pygpt_net/core/render/markdown/parser.py +0 -2
  23. pygpt_net/data/config/config.json +5 -6
  24. pygpt_net/data/config/models.json +3 -3
  25. pygpt_net/data/config/settings.json +2 -38
  26. pygpt_net/data/locale/locale.de.ini +64 -1
  27. pygpt_net/data/locale/locale.en.ini +62 -3
  28. pygpt_net/data/locale/locale.es.ini +64 -1
  29. pygpt_net/data/locale/locale.fr.ini +64 -1
  30. pygpt_net/data/locale/locale.it.ini +64 -1
  31. pygpt_net/data/locale/locale.pl.ini +65 -2
  32. pygpt_net/data/locale/locale.uk.ini +64 -1
  33. pygpt_net/data/locale/locale.zh.ini +64 -1
  34. pygpt_net/data/locale/plugin.cmd_system.en.ini +62 -66
  35. pygpt_net/provider/agents/llama_index/flow_from_schema.py +2 -2
  36. pygpt_net/provider/core/config/patch.py +10 -1
  37. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +0 -6
  38. pygpt_net/tools/agent_builder/tool.py +42 -26
  39. pygpt_net/tools/agent_builder/ui/dialogs.py +60 -11
  40. pygpt_net/ui/__init__.py +2 -4
  41. pygpt_net/ui/dialog/about.py +58 -38
  42. pygpt_net/ui/dialog/db.py +142 -3
  43. pygpt_net/ui/dialog/preset.py +47 -8
  44. pygpt_net/ui/layout/toolbox/presets.py +52 -16
  45. pygpt_net/ui/widget/dialog/db.py +0 -0
  46. pygpt_net/ui/widget/lists/preset.py +644 -60
  47. pygpt_net/ui/widget/node_editor/command.py +10 -10
  48. pygpt_net/ui/widget/node_editor/config.py +157 -0
  49. pygpt_net/ui/widget/node_editor/editor.py +183 -151
  50. pygpt_net/ui/widget/node_editor/item.py +12 -11
  51. pygpt_net/ui/widget/node_editor/node.py +267 -12
  52. pygpt_net/ui/widget/node_editor/view.py +180 -63
  53. pygpt_net/ui/widget/tabs/output.py +1 -1
  54. pygpt_net/ui/widget/textarea/input.py +2 -2
  55. pygpt_net/utils.py +114 -2
  56. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.61.dist-info}/METADATA +11 -94
  57. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.61.dist-info}/RECORD +59 -58
  58. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.61.dist-info}/LICENSE +0 -0
  59. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.61.dist-info}/WHEEL +0 -0
  60. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.61.dist-info}/entry_points.txt +0 -0
@@ -6,15 +6,17 @@
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.24 00:00:00 #
9
+ # Updated Date: 2025.09.26 03:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from __future__ import annotations
13
13
  from typing import Optional, Tuple
14
14
 
15
- from PySide6.QtCore import Qt, QPointF, QRectF, QObject, Signal
16
- from PySide6.QtGui import QColor, QPainter, QPen, QTransform
17
- from PySide6.QtWidgets import QWidget, QGraphicsView, QGraphicsScene
15
+ from PySide6.QtCore import Qt, QPointF, QRectF, QObject, Signal, QSize, QEvent
16
+ from PySide6.QtGui import QColor, QPainter, QPen, QTransform, QIcon
17
+ from PySide6.QtWidgets import QWidget, QGraphicsView, QGraphicsScene, QPushButton, QHBoxLayout, QLabel
18
+
19
+ from .config import EditorConfig
18
20
 
19
21
 
20
22
  # ------------------------ Graphics View / Scene ------------------------
@@ -26,14 +28,7 @@ class NodeGraphicsView(QGraphicsView):
26
28
  - Ctrl + Mouse Wheel zooming
27
29
  - Middle Mouse Button panning
28
30
  - Rubber band selection
29
-
30
- Notes:
31
- - Space-panning is intentionally disabled to not conflict with typing in editors.
32
- - All keyboard shortcuts (e.g., Delete) are handled at the NodeEditor level.
33
-
34
- Args:
35
- scene: Shared QGraphicsScene instance for the editor.
36
- parent: Optional parent widget.
31
+ - Optional left-button panning only when global grab mode is enabled
37
32
  """
38
33
  def __init__(self, scene: QGraphicsScene, parent: Optional[QWidget] = None):
39
34
  super().__init__(scene, parent)
@@ -53,17 +48,10 @@ class NodeGraphicsView(QGraphicsView):
53
48
 
54
49
  self._panning = False
55
50
  self._last_pan_pos = None
51
+ self._global_grab_mode = False # when True, left button pans regardless of items
56
52
 
57
53
  def drawBackground(self, painter: QPainter, rect: QRectF):
58
- """Draw the checker grid in the background.
59
-
60
- The grid spacing is fixed (20 px). Colors are read from the owning NodeEditor
61
- instance if available, which allows dynamic theming.
62
-
63
- Args:
64
- painter: Active QPainter provided by Qt.
65
- rect: The exposed background rect to be filled.
66
- """
54
+ """Draw the checker grid in the background."""
67
55
  parent_editor = self.parent() # NodeEditor
68
56
  color_back = getattr(parent_editor, "_grid_back_color", QColor(35, 35, 38))
69
57
  color_pen = getattr(parent_editor, "_grid_pen_color", QColor(55, 55, 60))
@@ -83,15 +71,23 @@ class NodeGraphicsView(QGraphicsView):
83
71
  painter.drawLine(rect.left(), y, rect.right(), y)
84
72
  y += grid
85
73
 
86
- def keyPressEvent(self, e):
87
- """Pass-through: the view does not handle special keys.
74
+ def enterEvent(self, e: QEvent):
75
+ """Ensure cursor reflects grab mode when entering the view."""
76
+ if self._global_grab_mode and not self._panning:
77
+ self.viewport().setCursor(Qt.OpenHandCursor)
78
+ else:
79
+ self.viewport().setCursor(Qt.ArrowCursor)
80
+ super().enterEvent(e)
81
+
82
+ def leaveEvent(self, e: QEvent):
83
+ """Restore cursor on leave."""
84
+ self.viewport().setCursor(Qt.ArrowCursor)
85
+ super().leaveEvent(e)
88
86
 
89
- ESC and other keys are intentionally left for the host application/editor.
90
- """
87
+ def keyPressEvent(self, e):
91
88
  super().keyPressEvent(e)
92
89
 
93
90
  def keyReleaseEvent(self, e):
94
- """Pass-through for key release."""
95
91
  super().keyReleaseEvent(e)
96
92
 
97
93
  def wheelEvent(self, e):
@@ -102,14 +98,47 @@ class NodeGraphicsView(QGraphicsView):
102
98
  return
103
99
  super().wheelEvent(e)
104
100
 
101
+ def _begin_pan(self, e):
102
+ """Start panning from current mouse event position."""
103
+ self._panning = True
104
+ self._last_pan_pos = e.position()
105
+ # Use 'grab' during drag
106
+ self.viewport().setCursor(Qt.ClosedHandCursor)
107
+ e.accept()
108
+
109
+ def _end_pan(self, e):
110
+ """Stop panning and restore appropriate cursor."""
111
+ self._panning = False
112
+ if self._global_grab_mode:
113
+ self.viewport().setCursor(Qt.OpenHandCursor)
114
+ else:
115
+ self.viewport().setCursor(Qt.ArrowCursor)
116
+ e.accept()
117
+
118
+ def _clicked_on_empty(self, e) -> bool:
119
+ """Return True if the click is on empty scene space (no items)."""
120
+ try:
121
+ item = self.itemAt(int(e.position().x()), int(e.position().y()))
122
+ return item is None
123
+ except Exception:
124
+ return False
125
+
105
126
  def mousePressEvent(self, e):
106
- """Start panning with Middle Mouse Button; otherwise defer to base implementation."""
127
+ """Panning: MMB always; LMB only in global grab mode. Also clear selection on empty click."""
107
128
  if e.button() == Qt.MiddleButton:
108
- self._panning = True
109
- self._last_pan_pos = e.position()
110
- self.setCursor(Qt.ClosedHandCursor)
111
- e.accept()
129
+ self._begin_pan(e)
112
130
  return
131
+
132
+ if e.button() == Qt.LeftButton:
133
+ if self._global_grab_mode:
134
+ # Global grab enabled -> pan on LMB anywhere
135
+ self._begin_pan(e)
136
+ return
137
+ else:
138
+ # No global grab: clicking empty clears selection
139
+ if self._clicked_on_empty(e) and self.scene():
140
+ self.scene().clearSelection()
141
+
113
142
  super().mousePressEvent(e)
114
143
 
115
144
  def mouseMoveEvent(self, e):
@@ -124,11 +153,9 @@ class NodeGraphicsView(QGraphicsView):
124
153
  super().mouseMoveEvent(e)
125
154
 
126
155
  def mouseReleaseEvent(self, e):
127
- """Stop panning on Middle Mouse Button release; otherwise defer."""
128
- if self._panning and e.button() == Qt.MiddleButton:
129
- self._panning = False
130
- self.setCursor(Qt.ArrowCursor)
131
- e.accept()
156
+ """Stop panning on Middle or Left Mouse Button release; otherwise defer."""
157
+ if self._panning and e.button() in (Qt.MiddleButton, Qt.LeftButton):
158
+ self._end_pan(e)
132
159
  return
133
160
  super().mouseReleaseEvent(e)
134
161
 
@@ -141,15 +168,7 @@ class NodeGraphicsView(QGraphicsView):
141
168
  self._apply_zoom(1.0 / self._zoom_step)
142
169
 
143
170
  def _apply_zoom(self, factor: float):
144
- """Apply zoom scaling factor within configured bounds.
145
-
146
- Args:
147
- factor: Multiplicative factor to apply to the current zoom.
148
-
149
- Notes:
150
- The method clamps the result to [_min_zoom, _max_zoom] to prevent
151
- excessive zooming.
152
- """
171
+ """Apply zoom scaling factor within configured bounds."""
153
172
  new_zoom = self._zoom * factor
154
173
  if not (self._min_zoom <= new_zoom <= self._max_zoom):
155
174
  return
@@ -169,7 +188,6 @@ class NodeGraphicsView(QGraphicsView):
169
188
  if keep_center and self.viewport() is not None and self.viewport().rect().isValid():
170
189
  center_scene = self.mapToScene(self.viewport().rect().center())
171
190
 
172
- # Reset and apply new transform to avoid cumulative floating errors
173
191
  self.resetTransform()
174
192
  self._zoom = 1.0
175
193
  if abs(z - 1.0) > 1e-9:
@@ -180,25 +198,21 @@ class NodeGraphicsView(QGraphicsView):
180
198
  self.centerOn(center_scene)
181
199
 
182
200
  def get_scroll_values(self) -> Tuple[int, int]:
183
- """Return (horizontal, vertical) scrollbar values."""
184
201
  h = self.horizontalScrollBar().value() if self.horizontalScrollBar() else 0
185
202
  v = self.verticalScrollBar().value() if self.verticalScrollBar() else 0
186
203
  return int(h), int(v)
187
204
 
188
205
  def set_scroll_values(self, h: int, v: int):
189
- """Set horizontal and vertical scrollbar values."""
190
206
  if self.horizontalScrollBar():
191
207
  self.horizontalScrollBar().setValue(int(h))
192
208
  if self.verticalScrollBar():
193
209
  self.verticalScrollBar().setValue(int(v))
194
210
 
195
211
  def view_state(self) -> dict:
196
- """Return a serializable view state: zoom and scrollbars."""
197
212
  h, v = self.get_scroll_values()
198
213
  return {"zoom": float(self._zoom), "h": h, "v": v}
199
214
 
200
215
  def set_view_state(self, state: dict):
201
- """Apply a view state previously produced by view_state()."""
202
216
  if not isinstance(state, dict):
203
217
  return
204
218
  z = state.get("zoom") or state.get("scale")
@@ -213,35 +227,138 @@ class NodeGraphicsView(QGraphicsView):
213
227
  if h is not None:
214
228
  self.set_scroll_values(int(h), int(v if v is not None else 0))
215
229
  elif v is not None:
216
- # set vertical if only v present
217
230
  self.set_scroll_values(self.get_scroll_values()[0], int(v))
218
231
  except Exception:
219
232
  pass
220
233
 
234
+ def set_global_grab_mode(self, enabled: bool):
235
+ """Enable/disable global grab mode (left click pans anywhere)."""
236
+ self._global_grab_mode = bool(enabled)
237
+ if self._global_grab_mode:
238
+ self.viewport().setCursor(Qt.OpenHandCursor)
239
+ else:
240
+ if not self._panning:
241
+ self.viewport().setCursor(Qt.ArrowCursor)
242
+
243
+
244
+ class NodeViewOverlayControls(QWidget):
245
+ """Small overlay with three buttons (Grab toggle, Zoom Out, Zoom In) anchored top-right."""
246
+
247
+ grabToggled = Signal(bool)
248
+ zoomInClicked = Signal()
249
+ zoomOutClicked = Signal()
250
+
251
+ def __init__(self, parent: Optional[QWidget] = None):
252
+ super().__init__(parent)
253
+ self.setObjectName("NodeViewOverlayControls")
254
+ self.setAttribute(Qt.WA_StyledBackground, True)
255
+
256
+ layout = QHBoxLayout(self)
257
+ # Bigger spacing to visually add padding around buttons
258
+ layout.setContentsMargins(0, 0, 0, 0)
259
+ layout.setSpacing(8)
260
+
261
+ cfg = self._cfg()
262
+
263
+ # Grab (toggle)
264
+ self.btnGrab = QPushButton(self)
265
+ self.btnGrab.setCheckable(True)
266
+ self.btnGrab.setToolTip(cfg.overlay_grab_tooltip())
267
+ self.btnGrab.setIcon(QIcon(":/icons/drag.svg"))
268
+ self.btnGrab.setIconSize(QSize(20, 20))
269
+ self.btnGrab.setMinimumSize(32, 32)
270
+
271
+ # Zoom Out (placed before Zoom In)
272
+ self.btnZoomOut = QPushButton(self)
273
+ self.btnZoomOut.setToolTip(cfg.overlay_zoom_out_tooltip())
274
+ self.btnZoomOut.setIcon(QIcon(":/icons/zoom_out.svg"))
275
+ self.btnZoomOut.setIconSize(QSize(20, 20))
276
+ self.btnZoomOut.setMinimumSize(32, 32)
277
+
278
+ # Zoom In
279
+ self.btnZoomIn = QPushButton(self)
280
+ self.btnZoomIn.setToolTip(cfg.overlay_zoom_in_tooltip())
281
+ self.btnZoomIn.setIcon(QIcon(":/icons/zoom_in.svg"))
282
+ self.btnZoomIn.setIconSize(QSize(20, 20))
283
+ self.btnZoomIn.setMinimumSize(32, 32)
284
+
285
+ layout.addWidget(self.btnGrab)
286
+ layout.addWidget(self.btnZoomIn)
287
+ layout.addWidget(self.btnZoomOut)
288
+
289
+ self.btnGrab.toggled.connect(self.grabToggled.emit)
290
+ self.btnZoomIn.clicked.connect(self.zoomInClicked.emit)
291
+ self.btnZoomOut.clicked.connect(self.zoomOutClicked.emit)
292
+
293
+ self.show()
294
+
295
+ def _cfg(self) -> EditorConfig:
296
+ p = self.parent()
297
+ c = getattr(p, "config", None) if p is not None else None
298
+ return c if isinstance(c, EditorConfig) else EditorConfig()
299
+
300
+
301
+ class NodeViewStatusLabel(QWidget):
302
+ """Fixed status overlay pinned to bottom-left that shows node type counts."""
303
+
304
+ def __init__(self, parent: Optional[QWidget] = None):
305
+ super().__init__(parent)
306
+ self.setObjectName("NodeViewStatusLabel")
307
+ self.setAttribute(Qt.WA_StyledBackground, True)
308
+
309
+ self._lbl = QLabel(self)
310
+ cfg = self._cfg()
311
+ self._lbl.setText(cfg.status_no_nodes())
312
+
313
+ layout = QHBoxLayout(self)
314
+ layout.setContentsMargins(8, 4, 8, 4) # some padding around the text
315
+ layout.setSpacing(0)
316
+ layout.addWidget(self._lbl)
317
+
318
+ self.adjustSize()
319
+ self.show()
320
+
321
+ def _cfg(self) -> EditorConfig:
322
+ p = self.parent()
323
+ c = getattr(p, "config", None) if p is not None else None
324
+ return c if isinstance(c, EditorConfig) else EditorConfig()
325
+
326
+ def set_text(self, text: str):
327
+ self._lbl.setText(text)
328
+ # Safe: adjustSize() uses sizeHint(), which no longer calls adjustSize()
329
+ self.adjustSize()
330
+
331
+ def sizeHint(self):
332
+ """Return hint based on layout/label without calling adjustSize()."""
333
+ if self.layout() is not None:
334
+ return self.layout().sizeHint()
335
+ return self._lbl.sizeHint()
336
+
221
337
 
222
338
  class NodeGraphicsScene(QGraphicsScene):
223
339
  """Graphics scene extended with custom context menu emission."""
224
340
  sceneContextRequested = Signal(QPointF)
225
341
 
226
342
  def __init__(self, parent: Optional[QObject] = None):
227
- """Initialize the scene and set a very large scene rect.
228
-
229
- Using a large default rect avoids sudden scene rect changes while panning/zooming.
230
- """
343
+ """Initialize the scene and set a very large scene rect."""
231
344
  super().__init__(parent)
232
345
  self.setSceneRect(-5000, -5000, 10000, 10000)
233
346
 
234
347
  def contextMenuEvent(self, event):
235
- """Emit a scene-level context menu request when clicking empty space.
236
-
237
- If the click is not on any item, the signal sceneContextRequested is emitted with
238
- the scene position. Otherwise, default handling is used (propagating to items).
239
- """
348
+ """Emit a scene-level context menu request when clicking empty space."""
240
349
  transform = self.views()[0].transform() if self.views() else QTransform()
241
350
  item = self.itemAt(event.scenePos(), transform)
242
351
  if item is None:
243
- self.sceneContextRequested.emit(event.scenePos())
352
+ # Respect external edit permission if available on parent editor
353
+ editor = self.parent()
354
+ allowed = True
355
+ try:
356
+ if hasattr(editor, "editing_allowed") and callable(editor.editing_allowed):
357
+ allowed = bool(editor.editing_allowed())
358
+ except Exception:
359
+ allowed = False
360
+ if allowed:
361
+ self.sceneContextRequested.emit(event.scenePos())
244
362
  event.accept()
245
363
  return
246
- super().contextMenuEvent(event)
247
-
364
+ super().contextMenuEvent(event)
@@ -311,7 +311,7 @@ class AddButton(QPushButton):
311
311
  )
312
312
  self.setObjectName('tab-add')
313
313
  self.setProperty('tabAdd', True)
314
- self.setToolTip(trans('action.tab.add.chat'))
314
+ self.setToolTip(trans('action.tab.add.chat.tooltip'))
315
315
 
316
316
  def mousePressEvent(self, event):
317
317
  """
@@ -94,8 +94,8 @@ class ChatInput(QTextEdit):
94
94
  key="web",
95
95
  icon=self.ICON_WEB_OFF,
96
96
  alt_icon=self.ICON_WEB_ON,
97
- tooltip=trans('icon.remote_tool.web'),
98
- alt_tooltip=trans('icon.remote_tool.web'),
97
+ tooltip=trans('icon.remote_tool.web.disabled'),
98
+ alt_tooltip=trans('icon.remote_tool.web.enabled'),
99
99
  callback=self.action_toggle_web,
100
100
  visible=True,
101
101
  )
pygpt_net/utils.py CHANGED
@@ -6,15 +6,19 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.19 07:00:00 #
9
+ # Updated Date: 2025.09.25 12:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import json
13
13
  import os
14
14
  import re
15
+ import math
16
+
15
17
  from datetime import datetime
16
18
  from contextlib import contextmanager
17
19
  from typing import Any
20
+ from decimal import Decimal, ROUND_HALF_UP, InvalidOperation
21
+ from typing import Sequence
18
22
 
19
23
  from PySide6 import QtCore, QtGui
20
24
  from PySide6.QtWidgets import QApplication
@@ -381,4 +385,112 @@ def mem_clean(force: bool = False) -> bool:
381
385
  '''
382
386
  except Exception as e:
383
387
  print(e)
384
- return ok
388
+ return ok
389
+
390
+ def short_num(value,
391
+ *,
392
+ base: int = 1000,
393
+ suffixes: Sequence[str] = ("", "k", "M", "B", "T", "P", "E"),
394
+ max_decimals: int = 1,
395
+ decimal_sep: str = ",") -> str:
396
+ """
397
+ Compact human-readable formatter for numbers with suffixes (k, M, B, ...).
398
+
399
+ Rules:
400
+ - abs(value) < base -> return the value without a suffix
401
+ - otherwise -> divide by base^n and append suffix
402
+ - decimals:
403
+ < 10 -> up to 2 (bounded by max_decimals)
404
+ < 100 -> up to 1 (bounded by max_decimals)
405
+ >= 100 -> 0
406
+ - rounding: ROUND_HALF_UP
407
+ - auto "carry" to the next suffix after rounding (e.g., 999.95k -> 1M)
408
+
409
+ Params:
410
+ - base: 1000 for general numbers, 1024 for bytes, etc.
411
+ - suffixes: first item must be "" (for < base)
412
+ - max_decimals: upper bound for fractional digits
413
+ - decimal_sep: decimal separator in the output
414
+ """
415
+
416
+ # --- helpers hidden inside (closure) ---
417
+
418
+ def _to_decimal(v) -> Decimal:
419
+ """Convert supported inputs to Decimal; keep NaN/Inf as-is."""
420
+ if isinstance(v, Decimal):
421
+ return v
422
+ if isinstance(v, int):
423
+ return Decimal(v)
424
+ if isinstance(v, float):
425
+ if math.isnan(v):
426
+ return Decimal("NaN")
427
+ if math.isinf(v):
428
+ return Decimal("Infinity") if v > 0 else Decimal("-Infinity")
429
+ # str() to avoid binary float artefacts
430
+ return Decimal(str(v))
431
+ try:
432
+ return Decimal(str(v))
433
+ except (InvalidOperation, ValueError, TypeError):
434
+ raise TypeError("short_num(value): value must be numeric (int/float/Decimal) "
435
+ "or a string parsable to a number.")
436
+
437
+ def _decimals_for(scaled: Decimal) -> int:
438
+ """Pick number of decimals based on magnitude."""
439
+ if max_decimals <= 0:
440
+ return 0
441
+ if scaled < 10:
442
+ return min(2, max_decimals)
443
+ if scaled < 100:
444
+ return min(1, max_decimals)
445
+ return 0
446
+
447
+ def _round_dec(d: Decimal, decimals: int) -> Decimal:
448
+ """ROUND_HALF_UP to the requested decimals."""
449
+ if decimals <= 0:
450
+ return d.quantize(Decimal("1"), rounding=ROUND_HALF_UP)
451
+ q = Decimal(1).scaleb(-decimals) # 10**(-decimals)
452
+ return d.quantize(q, rounding=ROUND_HALF_UP)
453
+
454
+ def _strip_trailing_zeros(s: str) -> str:
455
+ """Remove trailing zeros and trailing decimal point if needed."""
456
+ if "." in s:
457
+ s = s.rstrip("0").rstrip(".")
458
+ return s
459
+
460
+ # --- main logic ---
461
+
462
+ d = _to_decimal(value)
463
+ if not d.is_finite():
464
+ # For NaN/Inf just echo back Python-ish text
465
+ return str(value)
466
+
467
+ sign = "-" if d < 0 else ""
468
+ d = abs(d)
469
+
470
+ # For values below base, return "as is" (normalized, no suffix)
471
+ if d < base:
472
+ s = _strip_trailing_zeros(f"{d.normalize():f}")
473
+ return (sign + s).replace(".", decimal_sep)
474
+
475
+ # Find initial suffix tier
476
+ idx = 0
477
+ last_idx = len(suffixes) - 1
478
+ while d >= base and idx < last_idx:
479
+ d = d / base
480
+ idx += 1
481
+
482
+ # Choose decimals, round, then handle possible carry to next suffix
483
+ decimals = _decimals_for(d)
484
+ d = _round_dec(d, decimals)
485
+
486
+ while d >= base and idx < last_idx:
487
+ # Carry over (e.g., 999.95k -> 1000.00k -> 1.00M)
488
+ d = d / base
489
+ idx += 1
490
+ decimals = _decimals_for(d)
491
+ d = _round_dec(d, decimals)
492
+
493
+ # Format, trim zeros, apply custom decimal separator, attach suffix
494
+ out = f"{d:.{decimals}f}"
495
+ out = _strip_trailing_zeros(out).replace(".", decimal_sep)
496
+ return f"{sign}{out}{suffixes[idx]}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pygpt-net
3
- Version: 2.6.60
3
+ Version: 2.6.61
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.60** | build: **2025-09-25** | Python: **>=3.10, <3.14**
120
+ Release: **2.6.61** | 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
  >
@@ -2224,7 +2224,8 @@ Rules:
2224
2224
  - content must contain the user-facing answer (you may include structured data as JSON or Markdown inside content).
2225
2225
  - Do NOT add any commentary outside of the JSON. No leading or trailing text.
2226
2226
  - If using tools, still return the final JSON with tool results summarized in content.
2227
- - Human-friendly route names: <friendly>
2227
+ - Human-friendly route names: <names>
2228
+ - Human-friendly route roles (optional): <roles>
2228
2229
 
2229
2230
  <here begins your system instruction>
2230
2231
  ```
@@ -2455,8 +2456,6 @@ Config -> Settings...
2455
2456
 
2456
2457
  - `Store dialog window positions`: Enable or disable dialogs positions store/restore, Default: True.
2457
2458
 
2458
- - `Use theme colors in chat window`: Use color theme in chat window, Default: True.
2459
-
2460
2459
  **Code syntax**
2461
2460
 
2462
2461
  - `Code syntax highlight`: Syntax highlight theme in code blocks. `WebEngine / Chromium` render mode only.
@@ -2523,8 +2522,6 @@ Config -> Settings...
2523
2522
 
2524
2523
  - `Open URLs in built-in browser`: Enable this option to open all URLs in the built-in browser (Chromium) instead of an external browser. Default: False.
2525
2524
 
2526
- - `Convert lists to paragraphs`: If enabled, lists (ul, ol) will be converted to paragraphs (p), Default: True.
2527
-
2528
2525
  - `Model used for auto-summary`: Model used for context auto-summary (generating titles in context list) (default: *gpt-4o-mini*). **Tip:** If you prefer to use local models, you should change the model here as well
2529
2526
 
2530
2527
  **Remote tools**
@@ -3724,6 +3721,13 @@ may consume additional tokens that are not displayed in the main window.
3724
3721
 
3725
3722
  ## Recent changes:
3726
3723
 
3724
+ **2.6.61 (2025-09-26)**
3725
+
3726
+ - Enhanced the agents node editor, custom agent flow, and instruction following.
3727
+ - Added drag-and-drop and reordering functionality to the presets list.
3728
+ - Added statistics for response tokens, including time elapsed and tokens per second.
3729
+ - Improved UI/UX.
3730
+
3727
3731
  **2.6.60 (2025-09-25)**
3728
3732
 
3729
3733
  - Added a new tool: Agents Builder - allowing visual design of agent workflows using nodes - available in Tools -> Agents Builder (beta).
@@ -3744,93 +3748,6 @@ may consume additional tokens that are not displayed in the main window.
3744
3748
  - Improved: The local web search plugin has been enhanced to retrieve multiple URLs at once.
3745
3749
  - Added: Use proxy switch in Settings.
3746
3750
 
3747
- **2.6.56 (2025-09-22)**
3748
-
3749
- - Optimized: Memory usage and performance in streaming and rendering large contexts.
3750
- - Added: Copy to clipboard functionality in user messages.
3751
- - Added: Manual scroll in plain-text mode.
3752
-
3753
- **2.6.55 (2025-09-18)**
3754
-
3755
- - Fixed: Unnecessary context loading from the database.
3756
- - Optimized: Token count in the input field.
3757
-
3758
- **2.6.54 (2025-09-18)**
3759
-
3760
- - Added: Remote tools (like web search) are now also available in the Chat with Files and Agents (LlamaIndex) modes.
3761
- - Added: Two new plugins: Wolfram Alpha and OpenStreetMap.
3762
- - Fixed: Enabled local file-like schemes in links/images in the markdown-it parser.
3763
-
3764
- **2.6.53 (2025-09-17)**
3765
-
3766
- - Added: An icon to enable/disable the web search remote tool in the icon bar, along with remote web search functionality in OpenRouter (#135).
3767
- - Added: The ability to mute audio in real-time mode via the audio icon.
3768
-
3769
- **2.6.52 (2025-09-17)**
3770
-
3771
- - Added MCP plugin: Provides access to remote tools via the Model Context Protocol (MCP), including stdio, SSE, and Streamable HTTP transports, with per-server allow/deny filtering, Authorization header support, and a tools cache.
3772
- - Fixed: tab tooltips reload on profile switch.
3773
-
3774
- **2.6.51 (2025-09-16)**
3775
-
3776
- - Fix: Automatically reloading calendar notes.
3777
- - Fix: Context menu CSS background color for calendar items.
3778
-
3779
- **2.6.50 (2025-09-16)**
3780
-
3781
- - Optimized: Improved memory cleanup when switching profiles and unloading tabs.
3782
- - Fix: Resolved missing PID data in text output.
3783
- - Fix: Enhanced real-time parsing of execute tags.
3784
-
3785
- **2.6.49 (2025-09-16)**
3786
-
3787
- - Fixed: Occasional crashes when focusing on an output container unloaded from memory in the second column.
3788
-
3789
- **2.6.48 (2025-09-15)**
3790
-
3791
- - Added: auto-loading of next items to the list of contexts when scrolling to the end of the list.
3792
-
3793
- **2.6.47 (2025-09-15)**
3794
-
3795
- - Improved: Parsing of custom markup tags.
3796
- - Optimized: Switching profiles.
3797
-
3798
- **2.6.46 (2025-09-15)**
3799
-
3800
- - Added: Global proxy settings for all API SDKs.
3801
- - Fixed: xAI client configuration in Chat with Files.
3802
- - Fixed: Top margin in streaming container.
3803
- - Refactored: Debug workers.
3804
-
3805
- **2.6.45 (2025-09-13)**
3806
-
3807
- - Improved: Parsing of custom markup in the stream.
3808
- - Improved: Message block parsing moved to JavaScript.
3809
-
3810
- **2.6.44 (2025-09-12)**
3811
-
3812
- - Added: Auto-collapse for large user input blocks.
3813
- - Added: Configuration for syntax highlighting intervals.
3814
- - Improved: Visibility of label icons.
3815
- - Improved: Scrolling of code blocks.
3816
- - Fixed: Parsing of quotes in custom markdown blocks.
3817
-
3818
- **2.6.43 (2025-09-12)**
3819
-
3820
- - Fixed: preset restoration when switching profiles.
3821
- - Improved: faster application launch and exit.
3822
-
3823
- **2.6.42 (2025-09-11)**
3824
-
3825
- - Fixed: Save/load zoom factor in the chat window when switched via Ctrl + mouse wheel.
3826
- - Fixed: Break the first line in code blocks.
3827
- - Added: Configuration options for syntax highlight limits.
3828
- - Updated: SVG icons.
3829
-
3830
- **2.6.41 (2025-09-11)**
3831
-
3832
- - Rendering engine optimizations: markdown parsing moved to JS, reduced CPU usage, added auto-memory clearing, and more.
3833
-
3834
3751
  # Credits and links
3835
3752
 
3836
3753
  **Official website:** <https://pygpt.net>