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
@@ -392,10 +392,12 @@ class AgentBuilder(BaseTool):
392
392
  # Start
393
393
  registry.register(NodeTypeSpec(
394
394
  type_name="Flow/Start",
395
+ display_name=trans("node.editor.spec.start.title"),
395
396
  title=trans("node.editor.spec.start.title"),
396
397
  base_id="start",
397
398
  export_kind="start",
398
399
  bg_color="#2D5A27",
400
+ max_num=1, # per-layout limit
399
401
  properties=[
400
402
  PropertySpec(id="output", type="flow", name=trans("node.editor.property.output.name"), editable=False,
401
403
  allowed_inputs=0, allowed_outputs=1),
@@ -406,6 +408,7 @@ class AgentBuilder(BaseTool):
406
408
  # Agent
407
409
  registry.register(NodeTypeSpec(
408
410
  type_name="Flow/Agent",
411
+ display_name=trans("node.editor.spec.agent.title"),
409
412
  title=trans("node.editor.spec.agent.title"),
410
413
  base_id="agent",
411
414
  export_kind="agent",
@@ -435,6 +438,7 @@ class AgentBuilder(BaseTool):
435
438
  # Memory
436
439
  registry.register(NodeTypeSpec(
437
440
  type_name="Flow/Memory",
441
+ display_name=trans("node.editor.spec.memory.title"),
438
442
  title=trans("node.editor.spec.memory.title"),
439
443
  base_id="mem",
440
444
  export_kind="memory",
@@ -449,10 +453,12 @@ class AgentBuilder(BaseTool):
449
453
  # End
450
454
  registry.register(NodeTypeSpec(
451
455
  type_name="Flow/End",
456
+ display_name=trans("node.editor.spec.end.title"),
452
457
  title=trans("node.editor.spec.end.title"),
453
458
  base_id="end",
454
459
  export_kind="end",
455
460
  bg_color="#6B2E2E",
461
+ max_num=1, # per-layout limit
456
462
  properties=[
457
463
  PropertySpec(id="input", type="flow", name=trans("node.editor.property.input.name"), editable=False,
458
464
  allowed_inputs=-1, allowed_outputs=0),
@@ -77,49 +77,8 @@ class Builder:
77
77
  registry=registry
78
78
  ) # parent == dialog
79
79
 
80
- theme = self.window.core.config.get("theme")
81
- if theme.startswith("light"):
82
- style = """
83
- NodeEditor {
84
- qproperty-gridBackColor: #ffffff;
85
- qproperty-gridPenColor: #eaeaea;
86
-
87
- qproperty-nodeBackgroundColor: #2d2f34;
88
- qproperty-nodeBorderColor: #4b4f57;
89
- qproperty-nodeSelectionColor: #ff9900;
90
- qproperty-nodeTitleColor: #3a3d44;
91
-
92
- qproperty-portInputColor: #66b2ff;
93
- qproperty-portOutputColor: #70e070;
94
- qproperty-portConnectedColor: #ffd166;
95
-
96
- qproperty-edgeColor: #c0c0c0;
97
- qproperty-edgeSelectedColor: #ff8a5c;
98
- }
99
- """
100
- else:
101
- style = """
102
- NodeEditor {
103
- qproperty-gridBackColor: #242629;
104
- qproperty-gridPenColor: #3b3f46;
105
-
106
- qproperty-nodeBackgroundColor: #2d2f34;
107
- qproperty-nodeBorderColor: #4b4f57;
108
- qproperty-nodeSelectionColor: #ff9900;
109
- qproperty-nodeTitleColor: #3a3d44;
110
-
111
- qproperty-portInputColor: #66b2ff;
112
- qproperty-portOutputColor: #70e070;
113
- qproperty-portConnectedColor: #ffd166;
114
-
115
- qproperty-edgeColor: #c0c0c0;
116
- qproperty-edgeSelectedColor: #ff8a5c;
117
- }
118
- """
119
- editor.setStyleSheet(style)
120
80
  editor.on_clear = self.tool.clear
121
81
  editor.editing_allowed = self.tool.editing_allowed
122
-
123
82
  u.editor[id] = editor
124
83
 
125
84
  layout = QVBoxLayout()
@@ -1,3 +1,4 @@
1
+ # ui/layout/presets.py
1
2
  #!/usr/bin/env python3
2
3
  # -*- coding: utf-8 -*-
3
4
  # ================================================== #
@@ -6,7 +7,7 @@
6
7
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
8
  # MIT License #
8
9
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.09.26 03:00:00 #
10
+ # Updated Date: 2025.09.26 13:30:00 #
10
11
  # ================================================== #
11
12
 
12
13
  from PySide6 import QtCore
@@ -126,6 +127,16 @@ class Presets:
126
127
  models[self.id] = model
127
128
  view.setModel(model)
128
129
 
130
+ # Preserve current scroll position across model rebuild to avoid a visible jump to the top.
131
+ # This is applied while updates are disabled, then restored just before re-enabling them.
132
+ try:
133
+ v = view.verticalScrollBar().value()
134
+ h = view.horizontalScrollBar().value()
135
+ view.set_pending_v_scroll(v)
136
+ view.set_pending_h_scroll(h)
137
+ except Exception:
138
+ pass
139
+
129
140
  # Block user input during model rebuild to avoid crashes on quick clicks
130
141
  view.begin_model_update()
131
142
 
@@ -193,5 +204,6 @@ class Presets:
193
204
  # Force repaint in case Qt defers layout until next input
194
205
  view.viewport().update()
195
206
 
196
- # Re-enable user interaction after the rebuild is fully done
207
+ # Clear one-shot pending scroll values and re-enable user interaction
208
+ view.clear_pending_scroll()
197
209
  view.end_model_update()
pygpt_net/ui/main.py CHANGED
@@ -22,7 +22,7 @@ from pygpt_net.controller import Controller
22
22
  from pygpt_net.tools import Tools
23
23
  from pygpt_net.ui import UI
24
24
  from pygpt_net.ui.widget.textarea.web import ChatWebOutput
25
- from pygpt_net.utils import get_app_meta, freeze_updates, set_env, has_env, get_env
25
+ from pygpt_net.utils import get_app_meta, freeze_updates, set_env, has_env, get_env, trans
26
26
 
27
27
 
28
28
  class MainWindow(QMainWindow, QtStyleTools):
@@ -356,7 +356,7 @@ class MainWindow(QMainWindow, QtStyleTools):
356
356
  self.core.presets.save_all()
357
357
  print("Exiting...")
358
358
  print("")
359
- print("Do you like PyGPT? Support the development of the project: https://pygpt.net/#donate")
359
+ print(f"{trans('exit.msg')} https://pygpt.net/#donate")
360
360
 
361
361
  def changeEvent(self, event):
362
362
  """
@@ -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: 2024.04.12 10:00:00 #
9
+ # Updated Date: 2025.09.26 10:00:00 #
10
10
  # ================================================== #
11
11
 
12
+ import sys
12
13
  from PySide6.QtCore import Qt
13
14
  from PySide6.QtWidgets import QDialog, QLabel, QHBoxLayout, QVBoxLayout, QPushButton
14
15
 
@@ -32,6 +33,7 @@ class ConfirmDialog(QDialog):
32
33
  self.setWindowTitle(trans('dialog.confirm.title'))
33
34
  self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint) # always on top
34
35
 
36
+ # Buttons
35
37
  self.window.ui.nodes['dialog.confirm.btn.yes'] = QPushButton(trans('dialog.confirm.yes'))
36
38
  self.window.ui.nodes['dialog.confirm.btn.yes'].clicked.connect(
37
39
  lambda: self.window.controller.dialogs.confirm.accept(self.type, self.id, self.parent_object))
@@ -40,9 +42,24 @@ class ConfirmDialog(QDialog):
40
42
  self.window.ui.nodes['dialog.confirm.btn.no'].clicked.connect(
41
43
  lambda: self.window.controller.dialogs.confirm.dismiss(self.type, self.id))
42
44
 
45
+ # Always make the neutral action (No/Cancel) the default/active one.
46
+ # This ensures Enter triggers the safe option by default.
47
+ self.window.ui.nodes['dialog.confirm.btn.no'].setAutoDefault(True)
48
+ self.window.ui.nodes['dialog.confirm.btn.no'].setDefault(True)
49
+ self.window.ui.nodes['dialog.confirm.btn.no'].setFocus()
50
+ self.window.ui.nodes['dialog.confirm.btn.yes'].setAutoDefault(False)
51
+ self.window.ui.nodes['dialog.confirm.btn.yes'].setDefault(False)
52
+
53
+ # Bottom button row with platform-specific ordering
54
+ # Windows: affirmative on the left, neutral on the right
55
+ # Linux/macOS: neutral on the left, affirmative on the right
43
56
  bottom = QHBoxLayout()
44
- bottom.addWidget(self.window.ui.nodes['dialog.confirm.btn.no'])
45
- bottom.addWidget(self.window.ui.nodes['dialog.confirm.btn.yes'])
57
+ if self._affirmative_on_left():
58
+ bottom.addWidget(self.window.ui.nodes['dialog.confirm.btn.yes'])
59
+ bottom.addWidget(self.window.ui.nodes['dialog.confirm.btn.no'])
60
+ else:
61
+ bottom.addWidget(self.window.ui.nodes['dialog.confirm.btn.no'])
62
+ bottom.addWidget(self.window.ui.nodes['dialog.confirm.btn.yes'])
46
63
 
47
64
  self.layout = QVBoxLayout()
48
65
  self.message = QLabel("")
@@ -54,6 +71,13 @@ class ConfirmDialog(QDialog):
54
71
  self.layout.addLayout(bottom)
55
72
  self.setLayout(self.layout)
56
73
 
74
+ def _affirmative_on_left(self) -> bool:
75
+ """
76
+ Decide button order depending on the platform.
77
+ Returns True on Windows, False otherwise (Linux/macOS).
78
+ """
79
+ return sys.platform.startswith('win')
80
+
57
81
  def closeEvent(self, event):
58
82
  """
59
83
  Close event handler
@@ -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.02 15:00:00 #
9
+ # Updated Date: 2025.09.26 12:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import datetime
@@ -98,6 +98,11 @@ class PainterWidget(QWidget):
98
98
  self._autoScrollMinSpeed = 2 # px per tick (min)
99
99
  self._autoScrollMaxSpeed = 18 # px per tick (max)
100
100
 
101
+ # Pan (middle mouse) state
102
+ self._panning = False
103
+ self._panLastGlobalPos = QPoint()
104
+ self._cursorBeforePan = None # store/restore cursor shape while panning
105
+
101
106
  # Actions
102
107
  self._act_undo = QAction(QIcon(":/icons/undo.svg"), trans('action.undo'), self)
103
108
  self._act_undo.triggered.connect(self.undo)
@@ -1084,6 +1089,69 @@ class PainterWidget(QWidget):
1084
1089
  if scrolled or dx != 0 or dy != 0:
1085
1090
  self.update()
1086
1091
 
1092
+ # ---------- Pan (middle mouse drag) ----------
1093
+
1094
+ def _can_pan(self) -> bool:
1095
+ """
1096
+ Return True if widget is inside a scroll area and content is scrollable.
1097
+ """
1098
+ self._find_scroll_area()
1099
+ if self._scrollArea is None:
1100
+ return False
1101
+ hbar = self._scrollArea.horizontalScrollBar()
1102
+ vbar = self._scrollArea.verticalScrollBar()
1103
+ h_ok = hbar is not None and hbar.maximum() > hbar.minimum()
1104
+ v_ok = vbar is not None and vbar.maximum() > vbar.minimum()
1105
+ return h_ok or v_ok
1106
+
1107
+ def _start_pan(self, global_pos: QPoint):
1108
+ """
1109
+ Begin view panning with middle mouse button.
1110
+ """
1111
+ if self._panning:
1112
+ return
1113
+ self._panning = True
1114
+ self._panLastGlobalPos = QPoint(global_pos)
1115
+ # Store current cursor to restore later
1116
+ self._cursorBeforePan = QCursor(self.cursor())
1117
+ # Use a closed hand to indicate grabbing the canvas
1118
+ self.setCursor(QCursor(Qt.ClosedHandCursor))
1119
+ self.grabMouse()
1120
+
1121
+ def _update_pan(self, global_pos: QPoint):
1122
+ """
1123
+ Update scrollbars based on mouse movement delta in global coordinates.
1124
+ """
1125
+ if not self._panning or self._scrollArea is None:
1126
+ return
1127
+ dx = global_pos.x() - self._panLastGlobalPos.x()
1128
+ dy = global_pos.y() - self._panLastGlobalPos.y()
1129
+ self._panLastGlobalPos = QPoint(global_pos)
1130
+
1131
+ hbar = self._scrollArea.horizontalScrollBar()
1132
+ vbar = self._scrollArea.verticalScrollBar()
1133
+
1134
+ # Dragging the content to the right should reveal the left side -> subtract deltas
1135
+ if hbar is not None and hbar.maximum() > hbar.minimum():
1136
+ hbar.setValue(int(max(hbar.minimum(), min(hbar.maximum(), hbar.value() - dx))))
1137
+ if vbar is not None and vbar.maximum() > vbar.minimum():
1138
+ vbar.setValue(int(max(vbar.minimum(), min(vbar.maximum(), vbar.value() - dy))))
1139
+
1140
+ def _end_pan(self):
1141
+ """
1142
+ End panning and restore previous cursor.
1143
+ """
1144
+ if not self._panning:
1145
+ return
1146
+ self._panning = False
1147
+ self.releaseMouse()
1148
+ try:
1149
+ if self._cursorBeforePan is not None:
1150
+ # Restore previous cursor (do not guess based on mode/crop)
1151
+ self.setCursor(self._cursorBeforePan)
1152
+ finally:
1153
+ self._cursorBeforePan = None
1154
+
1087
1155
  # ---------- Events ----------
1088
1156
 
1089
1157
  def wheelEvent(self, event):
@@ -1109,6 +1177,14 @@ class PainterWidget(QWidget):
1109
1177
 
1110
1178
  :param event: Event
1111
1179
  """
1180
+ # Middle button: start panning if scrollable
1181
+ if event.button() == Qt.MiddleButton:
1182
+ if not (self.cropping and self._selecting) and not self.drawing and self._can_pan():
1183
+ gp = event.globalPosition().toPoint()
1184
+ self._start_pan(gp)
1185
+ event.accept()
1186
+ return
1187
+
1112
1188
  if event.button() == Qt.LeftButton:
1113
1189
  self._mouseDown = True
1114
1190
  if self.cropping:
@@ -1146,6 +1222,13 @@ class PainterWidget(QWidget):
1146
1222
 
1147
1223
  :param event: Event
1148
1224
  """
1225
+ # Update panning if active
1226
+ if self._panning and (event.buttons() & Qt.MiddleButton):
1227
+ gp = event.globalPosition().toPoint()
1228
+ self._update_pan(gp)
1229
+ event.accept()
1230
+ return
1231
+
1149
1232
  if self.cropping and self._selecting and (event.buttons() & Qt.LeftButton):
1150
1233
  self._selectionRect = QRect(self._selectionStart, self._to_canvas_point(event.position()))
1151
1234
  self.update()
@@ -1175,6 +1258,12 @@ class PainterWidget(QWidget):
1175
1258
 
1176
1259
  :param event: Event
1177
1260
  """
1261
+ # End panning on middle button release
1262
+ if event.button() == Qt.MiddleButton:
1263
+ self._end_pan()
1264
+ event.accept()
1265
+ return
1266
+
1178
1267
  if event.button() in (Qt.LeftButton, Qt.RightButton):
1179
1268
  self._mouseDown = False
1180
1269
  if self.cropping and self._selecting: