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.
- pygpt_net/CHANGELOG.txt +7 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/controller/chat/common.py +115 -6
- pygpt_net/controller/chat/input.py +4 -1
- pygpt_net/controller/presets/presets.py +121 -6
- pygpt_net/controller/settings/editor.py +0 -15
- pygpt_net/controller/theme/markdown.py +2 -5
- pygpt_net/controller/ui/ui.py +4 -7
- pygpt_net/core/agents/custom/__init__.py +7 -1
- pygpt_net/core/agents/custom/llama_index/factory.py +17 -6
- pygpt_net/core/agents/custom/llama_index/runner.py +35 -2
- pygpt_net/core/agents/custom/llama_index/utils.py +12 -1
- pygpt_net/core/agents/custom/router.py +45 -6
- pygpt_net/core/agents/custom/runner.py +2 -1
- pygpt_net/core/agents/custom/schema.py +3 -1
- pygpt_net/core/agents/custom/utils.py +13 -1
- pygpt_net/core/db/viewer.py +11 -5
- pygpt_net/core/node_editor/graph.py +18 -9
- pygpt_net/core/node_editor/models.py +9 -2
- pygpt_net/core/node_editor/types.py +3 -1
- pygpt_net/core/presets/presets.py +216 -29
- pygpt_net/core/render/markdown/parser.py +0 -2
- pygpt_net/data/config/config.json +5 -6
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/settings.json +2 -38
- pygpt_net/data/locale/locale.de.ini +64 -1
- pygpt_net/data/locale/locale.en.ini +62 -3
- pygpt_net/data/locale/locale.es.ini +64 -1
- pygpt_net/data/locale/locale.fr.ini +64 -1
- pygpt_net/data/locale/locale.it.ini +64 -1
- pygpt_net/data/locale/locale.pl.ini +65 -2
- pygpt_net/data/locale/locale.uk.ini +64 -1
- pygpt_net/data/locale/locale.zh.ini +64 -1
- pygpt_net/data/locale/plugin.cmd_system.en.ini +62 -66
- pygpt_net/provider/agents/llama_index/flow_from_schema.py +2 -2
- pygpt_net/provider/core/config/patch.py +10 -1
- pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +0 -6
- pygpt_net/tools/agent_builder/tool.py +42 -26
- pygpt_net/tools/agent_builder/ui/dialogs.py +60 -11
- pygpt_net/ui/__init__.py +2 -4
- pygpt_net/ui/dialog/about.py +58 -38
- pygpt_net/ui/dialog/db.py +142 -3
- pygpt_net/ui/dialog/preset.py +47 -8
- pygpt_net/ui/layout/toolbox/presets.py +52 -16
- pygpt_net/ui/widget/dialog/db.py +0 -0
- pygpt_net/ui/widget/lists/preset.py +644 -60
- pygpt_net/ui/widget/node_editor/command.py +10 -10
- pygpt_net/ui/widget/node_editor/config.py +157 -0
- pygpt_net/ui/widget/node_editor/editor.py +183 -151
- pygpt_net/ui/widget/node_editor/item.py +12 -11
- pygpt_net/ui/widget/node_editor/node.py +267 -12
- pygpt_net/ui/widget/node_editor/view.py +180 -63
- pygpt_net/ui/widget/tabs/output.py +1 -1
- pygpt_net/ui/widget/textarea/input.py +2 -2
- pygpt_net/utils.py +114 -2
- {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.61.dist-info}/METADATA +11 -94
- {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.61.dist-info}/RECORD +59 -58
- {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.61.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.61.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.61.dist-info}/entry_points.txt +0 -0
pygpt_net/CHANGELOG.txt
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
2.6.61 (2025-09-26)
|
|
2
|
+
|
|
3
|
+
- Enhanced the agents node editor, custom agent flow, and instruction following.
|
|
4
|
+
- Added drag-and-drop and reordering functionality to the presets list.
|
|
5
|
+
- Added statistics for response tokens, including time elapsed and tokens per second.
|
|
6
|
+
- Improved UI/UX.
|
|
7
|
+
|
|
1
8
|
2.6.60 (2025-09-25)
|
|
2
9
|
|
|
3
10
|
- Added a new tool: Agents Builder - allowing visual design of agent workflows using nodes - available in Tools -> Agents Builder (beta).
|
pygpt_net/__init__.py
CHANGED
|
@@ -6,15 +6,15 @@
|
|
|
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 00:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
__author__ = "Marcin Szczygliński"
|
|
13
13
|
__copyright__ = "Copyright 2025, Marcin Szczygliński"
|
|
14
14
|
__credits__ = ["Marcin Szczygliński"]
|
|
15
15
|
__license__ = "MIT"
|
|
16
|
-
__version__ = "2.6.
|
|
17
|
-
__build__ = "2025-09-
|
|
16
|
+
__version__ = "2.6.61"
|
|
17
|
+
__build__ = "2025-09-26"
|
|
18
18
|
__maintainer__ = "Marcin Szczygliński"
|
|
19
19
|
__github__ = "https://github.com/szczyglis-dev/py-gpt"
|
|
20
20
|
__report__ = "https://github.com/szczyglis-dev/py-gpt/issues"
|
|
@@ -6,10 +6,12 @@
|
|
|
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.25 12:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import os
|
|
13
|
+
from time import perf_counter
|
|
14
|
+
from typing import Callable, Optional
|
|
13
15
|
|
|
14
16
|
from PySide6.QtGui import QTextCursor
|
|
15
17
|
from PySide6.QtWidgets import QFileDialog, QApplication
|
|
@@ -18,7 +20,7 @@ from pygpt_net.core.events import Event, AppEvent, RenderEvent, KernelEvent
|
|
|
18
20
|
from pygpt_net.core.types import MODE_ASSISTANT, MODE_AUDIO
|
|
19
21
|
from pygpt_net.item.ctx import CtxItem
|
|
20
22
|
from pygpt_net.item.model import ModelItem
|
|
21
|
-
from pygpt_net.utils import trans
|
|
23
|
+
from pygpt_net.utils import trans, short_num
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
class Common:
|
|
@@ -30,6 +32,12 @@ class Common:
|
|
|
30
32
|
"""
|
|
31
33
|
self.window = window
|
|
32
34
|
self.initialized = False
|
|
35
|
+
# Counter (seconds, float) – current/last measured time
|
|
36
|
+
self.counter: float = 0.0
|
|
37
|
+
# Private start timestamp
|
|
38
|
+
self._t0: Optional[float] = None
|
|
39
|
+
# Optional number-shortening function (e.g., 10000 -> "10k"); if None, a fallback is used
|
|
40
|
+
self._shortener = None
|
|
33
41
|
|
|
34
42
|
def setup(self):
|
|
35
43
|
"""Set up UI"""
|
|
@@ -310,6 +318,7 @@ class Common:
|
|
|
310
318
|
dispatch(AppEvent(AppEvent.INPUT_STOPPED)) # app event
|
|
311
319
|
|
|
312
320
|
self.stop_client() # stop clients
|
|
321
|
+
self.reset_counter()
|
|
313
322
|
|
|
314
323
|
def stop_client(self):
|
|
315
324
|
"""Stop all clients"""
|
|
@@ -457,15 +466,115 @@ class Common:
|
|
|
457
466
|
f.write(str(text).strip())
|
|
458
467
|
self.window.update_status(f"{trans('status.saved')}: {os.path.basename(file_name)}")
|
|
459
468
|
|
|
469
|
+
# --- Timer control ---
|
|
470
|
+
|
|
471
|
+
def start_counter(self) -> None:
|
|
472
|
+
"""Start the timer – call when send the request."""
|
|
473
|
+
self._t0 = perf_counter()
|
|
474
|
+
|
|
475
|
+
def stop_counter(self) -> float:
|
|
476
|
+
"""
|
|
477
|
+
Stop the timer. Returns elapsed seconds and stores it in self.counter.
|
|
478
|
+
"""
|
|
479
|
+
if self._t0 is None:
|
|
480
|
+
raise RuntimeError("Timer was not started (start_counter()).")
|
|
481
|
+
self.counter = perf_counter() - self._t0
|
|
482
|
+
self._t0 = None
|
|
483
|
+
return self.counter
|
|
484
|
+
|
|
485
|
+
def get_counter(self) -> float:
|
|
486
|
+
"""
|
|
487
|
+
Current elapsed time (if running) or last measured time (in seconds).
|
|
488
|
+
"""
|
|
489
|
+
if self._t0 is not None:
|
|
490
|
+
return perf_counter() - self._t0
|
|
491
|
+
return self.counter
|
|
492
|
+
|
|
493
|
+
def reset_counter(self) -> None:
|
|
494
|
+
"""Reset the timer."""
|
|
495
|
+
self._t0 = None
|
|
496
|
+
self.counter = 0.0
|
|
497
|
+
|
|
498
|
+
# --- Calculation and formatting ---
|
|
499
|
+
|
|
500
|
+
def tokens_per_second(self, tokens_generated: int, seconds: Optional[float] = None) -> float:
|
|
501
|
+
"""
|
|
502
|
+
Average tokens/s for the given token count and time (defaults to current/last).
|
|
503
|
+
"""
|
|
504
|
+
sec = self.get_counter() if seconds is None else max(float(seconds), 0.0)
|
|
505
|
+
if sec <= 0.0:
|
|
506
|
+
return 0.0
|
|
507
|
+
return float(tokens_generated) / sec
|
|
508
|
+
|
|
509
|
+
def format_duration(self, seconds: Optional[float] = None, max_units: int = 2) -> str:
|
|
510
|
+
"""
|
|
511
|
+
Pretty duration: e.g., 800ms, 12s, 1m 12s, 3h 5m, 1d 2h (by default up to 2 units).
|
|
512
|
+
"""
|
|
513
|
+
sec = self.get_counter() if seconds is None else max(float(seconds), 0.0)
|
|
514
|
+
|
|
515
|
+
if sec < 1.0:
|
|
516
|
+
return f"{int(round(sec * 1000)):d}ms"
|
|
517
|
+
|
|
518
|
+
total = int(sec) # floor to whole seconds
|
|
519
|
+
days, rem = divmod(total, 86400)
|
|
520
|
+
hours, rem = divmod(rem, 3600)
|
|
521
|
+
minutes, seconds = divmod(rem, 60)
|
|
522
|
+
|
|
523
|
+
parts = []
|
|
524
|
+
if days:
|
|
525
|
+
parts.append(f"{days}d")
|
|
526
|
+
if hours:
|
|
527
|
+
parts.append(f"{hours}h")
|
|
528
|
+
if minutes:
|
|
529
|
+
parts.append(f"{minutes}m")
|
|
530
|
+
if seconds or not parts:
|
|
531
|
+
parts.append(f"{seconds}s")
|
|
532
|
+
|
|
533
|
+
return " ".join(parts[:max_units])
|
|
534
|
+
|
|
535
|
+
def _fallback_shorten(self, value: float) -> str:
|
|
536
|
+
"""
|
|
537
|
+
Simple fallback shortener (e.g., 15300 -> '15.3k').
|
|
538
|
+
"""
|
|
539
|
+
n = abs(value)
|
|
540
|
+
sign = "-" if value < 0 else ""
|
|
541
|
+
if n < 1_000:
|
|
542
|
+
return f"{sign}{int(round(n))}"
|
|
543
|
+
elif n < 1_000_000:
|
|
544
|
+
v = n / 1_000
|
|
545
|
+
s = f"{v:.1f}".rstrip("0").rstrip(".")
|
|
546
|
+
return f"{sign}{s}k"
|
|
547
|
+
elif n < 1_000_000_000:
|
|
548
|
+
v = n / 1_000_000
|
|
549
|
+
s = f"{v:.1f}".rstrip("0").rstrip(".")
|
|
550
|
+
return f"{sign}{s}M"
|
|
551
|
+
else:
|
|
552
|
+
v = n / 1_000_000_000
|
|
553
|
+
s = f"{v:.1f}".rstrip("0").rstrip(".")
|
|
554
|
+
return f"{sign}{s}B"
|
|
555
|
+
|
|
556
|
+
def format_stats(self, tokens_generated: int) -> str:
|
|
557
|
+
"""
|
|
558
|
+
Returns a string in the format: "<X> tokens/s - <duration>", e.g., "15k tokens/s - 1m 12s".
|
|
559
|
+
"""
|
|
560
|
+
tps = self.tokens_per_second(tokens_generated)
|
|
561
|
+
if self._shortener:
|
|
562
|
+
tps_str = self._shortener(tps if isinstance(tps, (int, float)) else float(tps))
|
|
563
|
+
else:
|
|
564
|
+
tps_str = self._fallback_shorten(tps)
|
|
565
|
+
duration = self.format_duration()
|
|
566
|
+
return f"{tps_str} tokens/s - {duration}"
|
|
567
|
+
|
|
460
568
|
def show_response_tokens(self, ctx: CtxItem):
|
|
461
569
|
"""
|
|
462
570
|
Update response tokens
|
|
463
571
|
|
|
464
572
|
:param ctx: CtxItem
|
|
465
573
|
"""
|
|
466
|
-
|
|
467
|
-
if ctx.
|
|
468
|
-
|
|
574
|
+
stats = ""
|
|
575
|
+
if ctx.output_tokens > 0 and self.get_counter() > 0:
|
|
576
|
+
stats = f" | {self.format_stats(ctx.output_tokens)}"
|
|
577
|
+
self.reset_counter()
|
|
469
578
|
self.window.update_status(
|
|
470
|
-
f"{trans('status.tokens')}: {ctx.input_tokens} + {ctx.output_tokens} = {ctx.total_tokens}{
|
|
579
|
+
f"{trans('status.tokens')}: {short_num(ctx.input_tokens)} + {short_num(ctx.output_tokens)} = {short_num(ctx.total_tokens)}{stats}"
|
|
471
580
|
)
|
|
@@ -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.
|
|
9
|
+
# Updated Date: 2025.09.25 12:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from typing import Optional, Any, Dict
|
|
@@ -206,6 +206,9 @@ class Input:
|
|
|
206
206
|
'mode': mode,
|
|
207
207
|
}))
|
|
208
208
|
|
|
209
|
+
# start timer
|
|
210
|
+
self.window.controller.chat.common.start_counter()
|
|
211
|
+
|
|
209
212
|
# send input to API
|
|
210
213
|
if mode == MODE_IMAGE:
|
|
211
214
|
controller.chat.image.send(
|
|
@@ -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 03:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import re
|
|
@@ -24,12 +24,14 @@ from pygpt_net.core.types import (
|
|
|
24
24
|
MODE_AGENT_OPENAI,
|
|
25
25
|
)
|
|
26
26
|
from pygpt_net.controller.presets.editor import Editor
|
|
27
|
+
# Editor controller
|
|
27
28
|
from pygpt_net.core.events import AppEvent
|
|
28
29
|
from pygpt_net.item.preset import PresetItem
|
|
29
30
|
from pygpt_net.utils import trans
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
_FILENAME_SANITIZE_RE = re.compile(r'[^a-zA-Z0-9_\-\.]')
|
|
34
|
+
# keep original validation (do not break other parts)
|
|
33
35
|
_VALIDATE_FILENAME_RE = re.compile(r'[^\w\s\-\.]')
|
|
34
36
|
|
|
35
37
|
|
|
@@ -76,7 +78,7 @@ class Presets:
|
|
|
76
78
|
|
|
77
79
|
def select(self, idx: int):
|
|
78
80
|
"""
|
|
79
|
-
Select preset
|
|
81
|
+
Select preset by list index (legacy)
|
|
80
82
|
|
|
81
83
|
:param idx: value of the list (row idx)
|
|
82
84
|
"""
|
|
@@ -94,6 +96,32 @@ class Presets:
|
|
|
94
96
|
if editor_ctrl.opened and editor_ctrl.current != preset_id:
|
|
95
97
|
self.editor.init(preset_id)
|
|
96
98
|
|
|
99
|
+
def select_by_id(self, preset_id: str):
|
|
100
|
+
"""
|
|
101
|
+
Select preset by explicit ID (robust for DnD-ordered views).
|
|
102
|
+
"""
|
|
103
|
+
if self.preset_change_locked():
|
|
104
|
+
return
|
|
105
|
+
if not preset_id:
|
|
106
|
+
return
|
|
107
|
+
w = self.window
|
|
108
|
+
mode = w.core.config.get('mode')
|
|
109
|
+
if not w.core.presets.has(mode, preset_id):
|
|
110
|
+
return
|
|
111
|
+
w.core.config.set("preset", preset_id)
|
|
112
|
+
if 'current_preset' not in w.core.config.data:
|
|
113
|
+
w.core.config.data['current_preset'] = {}
|
|
114
|
+
w.core.config.data['current_preset'][mode] = preset_id
|
|
115
|
+
self.select_model()
|
|
116
|
+
w.controller.ui.update()
|
|
117
|
+
w.controller.model.select_current()
|
|
118
|
+
w.dispatch(AppEvent(AppEvent.PRESET_SELECTED))
|
|
119
|
+
idx = w.core.presets.get_idx_by_id(mode, preset_id)
|
|
120
|
+
self.set_selected(idx)
|
|
121
|
+
editor_ctrl = w.controller.presets.editor
|
|
122
|
+
if editor_ctrl.opened and editor_ctrl.current != preset_id:
|
|
123
|
+
self.editor.init(preset_id)
|
|
124
|
+
|
|
97
125
|
def get_current(self) -> Optional[PresetItem]:
|
|
98
126
|
"""
|
|
99
127
|
Get current preset
|
|
@@ -584,6 +612,43 @@ class Presets:
|
|
|
584
612
|
if mode == MODE_ASSISTANT:
|
|
585
613
|
w.core.assistants.load()
|
|
586
614
|
|
|
615
|
+
def _nearest_id_after_delete(self, mode: str, idx: Optional[int], deleting_id: Optional[str]) -> Optional[str]:
|
|
616
|
+
"""
|
|
617
|
+
Compute the nearest neighbor to select after deletion:
|
|
618
|
+
- Prefer the next item (below) if exists;
|
|
619
|
+
- Otherwise choose the previous one (above);
|
|
620
|
+
- Returns None when no neighbor can be determined.
|
|
621
|
+
This uses the current view order for the given mode, including pinned current.<mode> at index 0.
|
|
622
|
+
"""
|
|
623
|
+
try:
|
|
624
|
+
w = self.window
|
|
625
|
+
data = w.core.presets.get_by_mode(mode) or {}
|
|
626
|
+
ids = list(data.keys())
|
|
627
|
+
if not ids:
|
|
628
|
+
return None
|
|
629
|
+
|
|
630
|
+
# If idx is invalid, try to resolve from deleting_id
|
|
631
|
+
if idx is None or idx < 0 or idx >= len(ids):
|
|
632
|
+
if deleting_id and deleting_id in ids:
|
|
633
|
+
idx = ids.index(deleting_id)
|
|
634
|
+
else:
|
|
635
|
+
return None
|
|
636
|
+
|
|
637
|
+
# Prefer below
|
|
638
|
+
if idx + 1 < len(ids):
|
|
639
|
+
cand = ids[idx + 1]
|
|
640
|
+
if cand and cand != deleting_id:
|
|
641
|
+
return cand
|
|
642
|
+
|
|
643
|
+
# Otherwise above
|
|
644
|
+
if idx - 1 >= 0:
|
|
645
|
+
cand = ids[idx - 1]
|
|
646
|
+
if cand and cand != deleting_id:
|
|
647
|
+
return cand
|
|
648
|
+
except Exception:
|
|
649
|
+
pass
|
|
650
|
+
return None
|
|
651
|
+
|
|
587
652
|
def delete(
|
|
588
653
|
self,
|
|
589
654
|
idx: Optional[int] = None,
|
|
@@ -607,10 +672,31 @@ class Presets:
|
|
|
607
672
|
msg=trans('confirm.preset.delete'),
|
|
608
673
|
)
|
|
609
674
|
return
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
675
|
+
|
|
676
|
+
# Determine neighbor only if the deleted preset is currently active.
|
|
677
|
+
# This keeps API semantics untouched and prevents unexpected selection changes.
|
|
678
|
+
is_current = (preset_id == w.core.config.get('preset'))
|
|
679
|
+
target_id = None
|
|
680
|
+
if is_current:
|
|
681
|
+
target_id = self._nearest_id_after_delete(mode, idx, preset_id)
|
|
682
|
+
|
|
683
|
+
# Remove from core (also removes file when True)
|
|
613
684
|
w.core.presets.remove(preset_id, True)
|
|
685
|
+
|
|
686
|
+
# When removing the active preset, jump to the nearest neighbor (below or above).
|
|
687
|
+
# If no neighbor can be determined, fall back to previous behavior (clear and let defaults apply).
|
|
688
|
+
if is_current:
|
|
689
|
+
if target_id and target_id in w.core.presets.items:
|
|
690
|
+
# Persist new selection in config (and keep current_preset mapping coherent)
|
|
691
|
+
w.core.config.set('preset', target_id)
|
|
692
|
+
if 'current_preset' not in w.core.config.data:
|
|
693
|
+
w.core.config.data['current_preset'] = {}
|
|
694
|
+
w.core.config.data['current_preset'][mode] = target_id
|
|
695
|
+
else:
|
|
696
|
+
# Fallback: clear selection to allow select_default() to pick the first available
|
|
697
|
+
w.core.config.set('preset', None)
|
|
698
|
+
w.ui.nodes['preset.prompt'].setPlainText("")
|
|
699
|
+
|
|
614
700
|
self.refresh(no_scroll=True)
|
|
615
701
|
w.update_status(trans('status.preset.deleted'))
|
|
616
702
|
|
|
@@ -715,4 +801,33 @@ class Presets:
|
|
|
715
801
|
|
|
716
802
|
def clear_selected(self):
|
|
717
803
|
"""Clear selected list"""
|
|
718
|
-
self.selected = []
|
|
804
|
+
self.selected = []
|
|
805
|
+
|
|
806
|
+
# ----------------------------
|
|
807
|
+
# Drag & drop ordering helpers
|
|
808
|
+
# ----------------------------
|
|
809
|
+
|
|
810
|
+
def persist_order_for_mode(self, mode: str, uuids: List[str]):
|
|
811
|
+
"""
|
|
812
|
+
Persist new order (by UUIDs) for given mode.
|
|
813
|
+
|
|
814
|
+
The special '*' preset (current.<mode>) is not included here and always pinned at index 0.
|
|
815
|
+
"""
|
|
816
|
+
w = self.window
|
|
817
|
+
cfg = w.core.config
|
|
818
|
+
order = cfg.get('presets_order') or {}
|
|
819
|
+
# Normalize to lists
|
|
820
|
+
if isinstance(order.get(mode), dict):
|
|
821
|
+
mapped = order.get(mode)
|
|
822
|
+
order[mode] = [mapped[k] for k in sorted(mapped.keys(), key=lambda x: int(x))]
|
|
823
|
+
order[mode] = [u for u in uuids if u]
|
|
824
|
+
cfg.set('presets_order', order)
|
|
825
|
+
|
|
826
|
+
def dnd_enabled(self) -> bool:
|
|
827
|
+
"""
|
|
828
|
+
Check if drag and drop is globally enabled in config.
|
|
829
|
+
"""
|
|
830
|
+
try:
|
|
831
|
+
return bool(self.window.core.config.get('presets.drag_and_drop.enabled'))
|
|
832
|
+
except Exception:
|
|
833
|
+
return False
|
|
@@ -51,11 +51,9 @@ class Editor:
|
|
|
51
51
|
self.window.ui.add_hook("update.config.font_size.ctx", self.hook_update)
|
|
52
52
|
self.window.ui.add_hook("update.config.font_size.toolbox", self.hook_update)
|
|
53
53
|
self.window.ui.add_hook("update.config.zoom", self.hook_update)
|
|
54
|
-
self.window.ui.add_hook("update.config.theme.markdown", self.hook_update)
|
|
55
54
|
self.window.ui.add_hook("update.config.vision.capture.enabled", self.hook_update)
|
|
56
55
|
self.window.ui.add_hook("update.config.vision.capture.auto", self.hook_update)
|
|
57
56
|
self.window.ui.add_hook("update.config.ctx.records.limit", self.hook_update)
|
|
58
|
-
self.window.ui.add_hook("update.config.ctx.convert_lists", self.hook_update)
|
|
59
57
|
self.window.ui.add_hook("update.config.ctx.records.separators", self.hook_update)
|
|
60
58
|
self.window.ui.add_hook("update.config.ctx.records.groups.separators", self.hook_update)
|
|
61
59
|
self.window.ui.add_hook("update.config.ctx.records.pinned.separators", self.hook_update)
|
|
@@ -176,10 +174,6 @@ class Editor:
|
|
|
176
174
|
value = self.window.core.config.get('theme.style')
|
|
177
175
|
self.window.controller.theme.toggle_style(value)
|
|
178
176
|
|
|
179
|
-
# convert lists
|
|
180
|
-
if self.config_changed('ctx.convert_lists'):
|
|
181
|
-
self.window.controller.ctx.refresh()
|
|
182
|
-
|
|
183
177
|
# access: voice control
|
|
184
178
|
if self.config_changed('access.voice_control'):
|
|
185
179
|
self.window.controller.access.voice.update()
|
|
@@ -266,19 +260,10 @@ class Editor:
|
|
|
266
260
|
self.window.core.config.set(key, value)
|
|
267
261
|
self.window.controller.ui.update_font_size()
|
|
268
262
|
|
|
269
|
-
# update markdown
|
|
270
|
-
elif key == "theme.markdown":
|
|
271
|
-
self.window.core.config.set(key, value)
|
|
272
|
-
self.window.controller.theme.markdown.update(force=True)
|
|
273
|
-
|
|
274
263
|
elif key == "render.code_syntax":
|
|
275
264
|
self.window.core.config.set(key, value)
|
|
276
265
|
self.window.controller.theme.toggle_syntax(value, update_menu=True)
|
|
277
266
|
|
|
278
|
-
elif key == "ctx.convert_lists":
|
|
279
|
-
self.window.core.config.set(key, value)
|
|
280
|
-
self.window.controller.ctx.refresh()
|
|
281
|
-
|
|
282
267
|
elif key == "ctx.records.separators":
|
|
283
268
|
self.window.core.config.set(key, value)
|
|
284
269
|
self.window.controller.ctx.update()
|
|
@@ -33,11 +33,8 @@ class Markdown:
|
|
|
33
33
|
"""
|
|
34
34
|
if force:
|
|
35
35
|
self.window.controller.ui.store_state() # store state before theme change
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
self.load()
|
|
39
|
-
else:
|
|
40
|
-
self.set_default()
|
|
36
|
+
|
|
37
|
+
self.load()
|
|
41
38
|
self.apply()
|
|
42
39
|
|
|
43
40
|
if force:
|
pygpt_net/controller/ui/ui.py
CHANGED
|
@@ -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.
|
|
9
|
+
# Updated Date: 2025.09.25 12:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from typing import Optional
|
|
@@ -15,7 +15,7 @@ from PySide6.QtGui import QColor
|
|
|
15
15
|
|
|
16
16
|
from pygpt_net.core.types import MODE_IMAGE
|
|
17
17
|
from pygpt_net.core.events import BaseEvent, Event
|
|
18
|
-
from pygpt_net.utils import trans
|
|
18
|
+
from pygpt_net.utils import trans, short_num
|
|
19
19
|
|
|
20
20
|
from .mode import Mode
|
|
21
21
|
from .tabs import Tabs
|
|
@@ -156,15 +156,12 @@ class UI:
|
|
|
156
156
|
attachments_tokens = self.window.controller.chat.attachment.get_current_tokens()
|
|
157
157
|
sum_tokens += attachments_tokens
|
|
158
158
|
|
|
159
|
-
ctx_string = f"{ctx_len} / {ctx_len_all} - {ctx_tokens} {trans('ctx.tokens')}"
|
|
159
|
+
ctx_string = f"{short_num(ctx_len)} / {short_num(ctx_len_all)} - {short_num(ctx_tokens)} {trans('ctx.tokens')}"
|
|
160
160
|
if ctx_string != self._last_ctx_string:
|
|
161
161
|
ui_nodes['prompt.context'].setText(ctx_string)
|
|
162
162
|
self._last_ctx_string = ctx_string
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
parsed_max_current = self.format_tokens(max_current)
|
|
166
|
-
|
|
167
|
-
input_string = f"{input_tokens} + {system_tokens} + {ctx_tokens} + {extra_tokens} + {attachments_tokens} = {parsed_sum} / {parsed_max_current}"
|
|
164
|
+
input_string = f"{short_num(input_tokens)} + {short_num(system_tokens)} + {short_num(ctx_tokens)} + {short_num(extra_tokens)} + {short_num(attachments_tokens)} = {short_num(sum_tokens)} / {short_num(max_current)}"
|
|
168
165
|
if input_string != self._last_input_string:
|
|
169
166
|
ui_nodes['input.counter'].setText(input_string)
|
|
170
167
|
self._last_input_string = input_string
|
|
@@ -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.25 14:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import copy
|
|
@@ -182,6 +182,12 @@ class Custom:
|
|
|
182
182
|
slots = node["slots"]
|
|
183
183
|
if "name" in slots and slots["name"]:
|
|
184
184
|
tab["label"] = slots["name"]
|
|
185
|
+
if "role" in slots:
|
|
186
|
+
opts["role"] = {
|
|
187
|
+
"type": "str",
|
|
188
|
+
"label": trans("agent.option.role"),
|
|
189
|
+
"default": slots["role"],
|
|
190
|
+
}
|
|
185
191
|
if "instruction" in slots:
|
|
186
192
|
opts["prompt"] = {
|
|
187
193
|
"type": "textarea",
|
|
@@ -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.25 14:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
@@ -32,7 +32,6 @@ class BuiltAgentLI:
|
|
|
32
32
|
class AgentFactoryLI:
|
|
33
33
|
"""
|
|
34
34
|
Build LlamaIndex ReActAgent/FunctionAgent from AgentNode + NodeRuntime and explicit LLM/tools.
|
|
35
|
-
Best practice: chat_history/max_iterations przekazujemy do konstruktora agenta.
|
|
36
35
|
"""
|
|
37
36
|
def __init__(self, window, logger) -> None:
|
|
38
37
|
self.window = window
|
|
@@ -45,7 +44,7 @@ class AgentFactoryLI:
|
|
|
45
44
|
node_runtime: NodeRuntime,
|
|
46
45
|
llm: Any, # LLM instance (z appki lub resolve_llm)
|
|
47
46
|
tools: List[Any], # BaseTool list
|
|
48
|
-
friendly_map: Dict[str,
|
|
47
|
+
friendly_map: Dict[str, Any],
|
|
49
48
|
force_router: bool = False,
|
|
50
49
|
chat_history: List[Any] = None,
|
|
51
50
|
max_iterations: int = 10,
|
|
@@ -62,23 +61,35 @@ class AgentFactoryLI:
|
|
|
62
61
|
|
|
63
62
|
node_tools = tools if (node_runtime.allow_local_tools or node_runtime.allow_remote_tools) else []
|
|
64
63
|
|
|
64
|
+
# Prefer FunctionAgent if the underlying LLM supports function-calling (recommended by LI).
|
|
65
|
+
# This yields more direct compliance with system_prompt for simple single-output tasks.
|
|
66
|
+
is_fc_model = False
|
|
67
|
+
try:
|
|
68
|
+
is_fc_model = bool(getattr(getattr(llm, "metadata", None), "is_function_calling_model", False))
|
|
69
|
+
except Exception:
|
|
70
|
+
is_fc_model = False
|
|
71
|
+
|
|
65
72
|
if multi_output:
|
|
66
|
-
agent_cls = FunctionAgent # routers
|
|
73
|
+
agent_cls = FunctionAgent # routers: keep JSON compliance
|
|
67
74
|
else:
|
|
68
|
-
agent_cls = FunctionAgent if
|
|
75
|
+
agent_cls = FunctionAgent if is_fc_model else ReActAgent
|
|
76
|
+
|
|
69
77
|
kwargs: Dict[str, Any] = {
|
|
70
78
|
"name": agent_name,
|
|
71
79
|
"system_prompt": instr,
|
|
72
80
|
"llm": llm,
|
|
73
81
|
"chat_history": chat_history or [],
|
|
74
82
|
"max_iterations": int(max_iterations),
|
|
83
|
+
# Provide a short description to reinforce the agent's purpose (if role present).
|
|
84
|
+
"description": (node_runtime.role or agent_name),
|
|
75
85
|
}
|
|
76
86
|
if node_tools:
|
|
77
87
|
kwargs["tools"] = coerce_li_tools(node_tools)
|
|
78
88
|
|
|
79
89
|
instance = agent_cls(**kwargs)
|
|
80
90
|
self.logger.debug(
|
|
81
|
-
f"[li] Built agent {node.id} ({agent_name}), multi_output={multi_output},
|
|
91
|
+
f"[li] Built agent {node.id} ({agent_name}), multi_output={multi_output}, "
|
|
92
|
+
f"routes={allowed_routes}, agent_cls={agent_cls.__name__}"
|
|
82
93
|
)
|
|
83
94
|
return BuiltAgentLI(
|
|
84
95
|
instance=instance,
|
|
@@ -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.25 14:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
@@ -210,6 +210,34 @@ class DynamicFlowWorkflowLI(Workflow):
|
|
|
210
210
|
def _friendly_map(self) -> Dict[str, str]:
|
|
211
211
|
return {aid: a.name or aid for aid, a in self.fs.agents.items()}
|
|
212
212
|
|
|
213
|
+
def _friendly_map_for_routes(self, route_ids: List[str]) -> Dict[str, Any]:
|
|
214
|
+
"""
|
|
215
|
+
Build a friendly map for the given route ids:
|
|
216
|
+
- Always include a human-friendly name.
|
|
217
|
+
- Include role only if provided in preset options or schema and non-empty.
|
|
218
|
+
"""
|
|
219
|
+
out: Dict[str, Any] = {}
|
|
220
|
+
for rid in route_ids or []:
|
|
221
|
+
a = self.fs.agents.get(rid)
|
|
222
|
+
name = (a.name if a and a.name else rid)
|
|
223
|
+
# Prefer preset option, then schema role
|
|
224
|
+
role_opt = None
|
|
225
|
+
try:
|
|
226
|
+
role_opt = self.option_get(rid, "role", None)
|
|
227
|
+
except Exception:
|
|
228
|
+
role_opt = None
|
|
229
|
+
role_schema = getattr(a, "role", None) if a is not None else None
|
|
230
|
+
role_val = None
|
|
231
|
+
if isinstance(role_opt, str) and role_opt.strip():
|
|
232
|
+
role_val = role_opt.strip()
|
|
233
|
+
elif isinstance(role_schema, str) and role_schema.strip():
|
|
234
|
+
role_val = role_schema.strip()
|
|
235
|
+
item = {"name": name}
|
|
236
|
+
if role_val:
|
|
237
|
+
item["role"] = role_val
|
|
238
|
+
out[rid] = item
|
|
239
|
+
return out
|
|
240
|
+
|
|
213
241
|
async def _emit(self, ctx: Context, ev: Any):
|
|
214
242
|
if self.dbg.event_echo:
|
|
215
243
|
self.logger.debug(f"[event] emit {ev.__class__.__name__}")
|
|
@@ -412,6 +440,7 @@ class DynamicFlowWorkflowLI(Workflow):
|
|
|
412
440
|
f"[runtime] model={getattr(node_rt.model,'name',str(node_rt.model))} "
|
|
413
441
|
f"allow_local={node_rt.allow_local_tools} allow_remote={node_rt.allow_remote_tools} "
|
|
414
442
|
f"instructions='{ellipsize(node_rt.instructions, self.dbg.preview_chars)}'"
|
|
443
|
+
f" role='{ellipsize(node_rt.role or '', self.dbg.preview_chars)}'"
|
|
415
444
|
)
|
|
416
445
|
|
|
417
446
|
llm_node = resolve_llm(self.window, node_rt.model, self.llm_base, self.stream)
|
|
@@ -430,13 +459,17 @@ class DynamicFlowWorkflowLI(Workflow):
|
|
|
430
459
|
f"user='{ellipsize(user_msg_text, self.dbg.preview_chars)}'"
|
|
431
460
|
)
|
|
432
461
|
|
|
462
|
+
# Prepare friendly map with optional roles for this node's allowed routes
|
|
463
|
+
allowed_routes_now = list(node.outputs or [])
|
|
464
|
+
friendly_map = self._friendly_map_for_routes(allowed_routes_now)
|
|
465
|
+
|
|
433
466
|
# Build agent (chat_history/max_iterations in ctor – best practice)
|
|
434
467
|
built = self.factory.build(
|
|
435
468
|
node=node,
|
|
436
469
|
node_runtime=node_rt,
|
|
437
470
|
llm=llm_node,
|
|
438
471
|
tools=tools_node,
|
|
439
|
-
friendly_map=
|
|
472
|
+
friendly_map=friendly_map,
|
|
440
473
|
chat_history=chat_history_msgs,
|
|
441
474
|
max_iterations=self.max_iterations,
|
|
442
475
|
)
|