pygpt-net 2.6.59__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 +11 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +9 -5
- pygpt_net/controller/__init__.py +1 -0
- pygpt_net/controller/chat/common.py +115 -6
- pygpt_net/controller/chat/input.py +4 -1
- pygpt_net/controller/presets/editor.py +442 -39
- 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 +281 -0
- pygpt_net/core/agents/custom/debug.py +64 -0
- pygpt_net/core/agents/custom/factory.py +109 -0
- pygpt_net/core/agents/custom/graph.py +71 -0
- pygpt_net/core/agents/custom/llama_index/__init__.py +10 -0
- pygpt_net/core/agents/custom/llama_index/factory.py +100 -0
- pygpt_net/core/agents/custom/llama_index/router_streamer.py +106 -0
- pygpt_net/core/agents/custom/llama_index/runner.py +562 -0
- pygpt_net/core/agents/custom/llama_index/stream.py +56 -0
- pygpt_net/core/agents/custom/llama_index/utils.py +253 -0
- pygpt_net/core/agents/custom/logging.py +50 -0
- pygpt_net/core/agents/custom/memory.py +51 -0
- pygpt_net/core/agents/custom/router.py +155 -0
- pygpt_net/core/agents/custom/router_streamer.py +187 -0
- pygpt_net/core/agents/custom/runner.py +455 -0
- pygpt_net/core/agents/custom/schema.py +127 -0
- pygpt_net/core/agents/custom/utils.py +193 -0
- pygpt_net/core/agents/provider.py +72 -7
- pygpt_net/core/agents/runner.py +7 -4
- pygpt_net/core/agents/runners/helpers.py +1 -1
- pygpt_net/core/agents/runners/llama_workflow.py +3 -0
- pygpt_net/core/agents/runners/openai_workflow.py +8 -1
- pygpt_net/core/db/viewer.py +11 -5
- pygpt_net/{ui/widget/builder → core/node_editor}/__init__.py +2 -2
- pygpt_net/core/{builder → node_editor}/graph.py +28 -226
- pygpt_net/core/node_editor/models.py +118 -0
- pygpt_net/core/node_editor/types.py +78 -0
- pygpt_net/core/node_editor/utils.py +17 -0
- pygpt_net/core/presets/presets.py +216 -29
- pygpt_net/core/render/markdown/parser.py +0 -2
- pygpt_net/core/render/web/renderer.py +10 -8
- 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 +63 -4
- 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/item/agent.py +5 -1
- pygpt_net/item/preset.py +19 -1
- pygpt_net/provider/agents/base.py +33 -2
- pygpt_net/provider/agents/llama_index/flow_from_schema.py +92 -0
- pygpt_net/provider/agents/openai/flow_from_schema.py +96 -0
- pygpt_net/provider/core/agent/json_file.py +11 -5
- 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 +233 -52
- pygpt_net/tools/agent_builder/ui/dialogs.py +172 -28
- pygpt_net/tools/agent_builder/ui/list.py +37 -10
- 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 +62 -8
- pygpt_net/ui/layout/toolbox/presets.py +52 -16
- pygpt_net/ui/main.py +1 -1
- pygpt_net/ui/widget/dialog/db.py +0 -0
- pygpt_net/ui/widget/lists/preset.py +644 -60
- pygpt_net/{core/builder → ui/widget/node_editor}/__init__.py +2 -2
- pygpt_net/ui/widget/node_editor/command.py +373 -0
- pygpt_net/ui/widget/node_editor/config.py +157 -0
- pygpt_net/ui/widget/node_editor/editor.py +2070 -0
- pygpt_net/ui/widget/node_editor/item.py +493 -0
- pygpt_net/ui/widget/node_editor/node.py +1460 -0
- pygpt_net/ui/widget/node_editor/utils.py +17 -0
- pygpt_net/ui/widget/node_editor/view.py +364 -0
- 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.59.dist-info → pygpt_net-2.6.61.dist-info}/METADATA +80 -93
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/RECORD +88 -61
- pygpt_net/core/agents/custom.py +0 -150
- pygpt_net/ui/widget/builder/editor.py +0 -2001
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/entry_points.txt +0 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2025.09.
|
|
9
|
+
# Updated Date: 2025.09.26 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
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# ================================================== #
|
|
4
|
+
# This file is a part of PYGPT package #
|
|
5
|
+
# Website: https://pygpt.net #
|
|
6
|
+
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
|
+
# MIT License #
|
|
8
|
+
# Created By : Marcin Szczygliński #
|
|
9
|
+
# Updated Date: 2025.09.25 14:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
import copy
|
|
13
|
+
from uuid import uuid4
|
|
14
|
+
|
|
15
|
+
from pygpt_net.core.types import MODEL_DEFAULT
|
|
16
|
+
from pygpt_net.item.agent import AgentItem
|
|
17
|
+
from pygpt_net.item.builder_layout import BuilderLayoutItem
|
|
18
|
+
from pygpt_net.provider.core.agent.json_file import JsonFileProvider
|
|
19
|
+
from pygpt_net.utils import trans
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Custom:
|
|
23
|
+
|
|
24
|
+
CUSTOM_AGENT_SUFFIX = " *" # suffix for custom agents in lists
|
|
25
|
+
|
|
26
|
+
def __init__(self, window=None):
|
|
27
|
+
"""
|
|
28
|
+
Custom agents core
|
|
29
|
+
|
|
30
|
+
:param window: Window instance
|
|
31
|
+
"""
|
|
32
|
+
self.window = window
|
|
33
|
+
self.provider = JsonFileProvider(window) # JSON file provider
|
|
34
|
+
self.agents = {} # dict of AgentItem
|
|
35
|
+
self.layout = None # BuilderLayoutItem
|
|
36
|
+
self.loaded = False
|
|
37
|
+
|
|
38
|
+
def load(self):
|
|
39
|
+
"""Load custom agents from provider"""
|
|
40
|
+
data = self.provider.load()
|
|
41
|
+
if "layout" in data:
|
|
42
|
+
self.layout = data["layout"]
|
|
43
|
+
if "agents" in data:
|
|
44
|
+
self.agents = data["agents"]
|
|
45
|
+
self.loaded = True
|
|
46
|
+
|
|
47
|
+
def reload(self):
|
|
48
|
+
"""Reload custom agents from provider"""
|
|
49
|
+
self.loaded = False
|
|
50
|
+
self.load()
|
|
51
|
+
|
|
52
|
+
def save(self):
|
|
53
|
+
"""Save custom agents to provider"""
|
|
54
|
+
self.provider.save(self.layout, self.agents)
|
|
55
|
+
|
|
56
|
+
def is_custom(self, agent_id: str) -> bool:
|
|
57
|
+
"""
|
|
58
|
+
Check if agent is custom
|
|
59
|
+
|
|
60
|
+
:param agent_id: agent ID
|
|
61
|
+
:return: True if custom
|
|
62
|
+
"""
|
|
63
|
+
if not self.loaded:
|
|
64
|
+
self.load()
|
|
65
|
+
return agent_id in self.agents
|
|
66
|
+
|
|
67
|
+
def update_layout(self, layout: dict):
|
|
68
|
+
"""
|
|
69
|
+
Update current layout of custom agents editor
|
|
70
|
+
|
|
71
|
+
:param layout: layout dict
|
|
72
|
+
"""
|
|
73
|
+
if self.layout is None:
|
|
74
|
+
self.layout = BuilderLayoutItem()
|
|
75
|
+
self.layout.data = layout
|
|
76
|
+
|
|
77
|
+
def reset(self):
|
|
78
|
+
"""Reset custom agents"""
|
|
79
|
+
self.agents = {}
|
|
80
|
+
self.layout = None
|
|
81
|
+
self.loaded = False
|
|
82
|
+
self.provider.truncate()
|
|
83
|
+
|
|
84
|
+
def get_layout(self) -> dict:
|
|
85
|
+
"""
|
|
86
|
+
Get layout of custom agents
|
|
87
|
+
|
|
88
|
+
:return: layout dict
|
|
89
|
+
"""
|
|
90
|
+
if not self.loaded:
|
|
91
|
+
self.load()
|
|
92
|
+
return self.layout
|
|
93
|
+
|
|
94
|
+
def get_agents(self) -> dict:
|
|
95
|
+
"""
|
|
96
|
+
Get custom agents
|
|
97
|
+
|
|
98
|
+
:return: dict of AgentItem
|
|
99
|
+
"""
|
|
100
|
+
if not self.loaded:
|
|
101
|
+
self.load()
|
|
102
|
+
return self.agents
|
|
103
|
+
|
|
104
|
+
def get_choices(self) -> list:
|
|
105
|
+
"""
|
|
106
|
+
Get custom agents choices
|
|
107
|
+
|
|
108
|
+
:return: list of dict with 'id' and 'name'
|
|
109
|
+
"""
|
|
110
|
+
if not self.loaded:
|
|
111
|
+
self.load()
|
|
112
|
+
return [{agent.id: f"{agent.name}{self.CUSTOM_AGENT_SUFFIX}"} for agent in self.agents.values()]
|
|
113
|
+
|
|
114
|
+
def get_agent(self, agent_id: str):
|
|
115
|
+
"""
|
|
116
|
+
Get custom agent by ID
|
|
117
|
+
|
|
118
|
+
:param agent_id: agent ID
|
|
119
|
+
:return: AgentItem or None
|
|
120
|
+
"""
|
|
121
|
+
if not self.loaded:
|
|
122
|
+
self.load()
|
|
123
|
+
return self.agents.get(agent_id)
|
|
124
|
+
|
|
125
|
+
def get_ids(self) -> list:
|
|
126
|
+
"""
|
|
127
|
+
Get list of custom agent IDs
|
|
128
|
+
|
|
129
|
+
:return: list of agent IDs
|
|
130
|
+
"""
|
|
131
|
+
if not self.loaded:
|
|
132
|
+
self.load()
|
|
133
|
+
return list(self.agents.keys())
|
|
134
|
+
|
|
135
|
+
def get_schema(self, agent_id: str) -> list:
|
|
136
|
+
"""
|
|
137
|
+
Get schema of a specific custom agent
|
|
138
|
+
|
|
139
|
+
:param agent_id: agent ID
|
|
140
|
+
:return: list with schema or empty list
|
|
141
|
+
"""
|
|
142
|
+
if not self.loaded:
|
|
143
|
+
self.load()
|
|
144
|
+
agent = self.agents.get(agent_id)
|
|
145
|
+
if agent:
|
|
146
|
+
return agent.schema
|
|
147
|
+
return []
|
|
148
|
+
|
|
149
|
+
def build_options(self, agent_id: str) -> dict:
|
|
150
|
+
"""
|
|
151
|
+
Build options for a specific custom agent
|
|
152
|
+
|
|
153
|
+
:param agent_id: agent ID
|
|
154
|
+
:return: dict with options or empty dict
|
|
155
|
+
"""
|
|
156
|
+
if not self.loaded:
|
|
157
|
+
self.load()
|
|
158
|
+
|
|
159
|
+
agent = self.agents.get(agent_id)
|
|
160
|
+
if not agent:
|
|
161
|
+
return {}
|
|
162
|
+
|
|
163
|
+
schema = agent.schema if agent else []
|
|
164
|
+
options = {}
|
|
165
|
+
for node in schema:
|
|
166
|
+
try:
|
|
167
|
+
if "type" in node and node["type"] == "agent":
|
|
168
|
+
if "id" in node:
|
|
169
|
+
sub_agent_id = node["id"]
|
|
170
|
+
tab = {
|
|
171
|
+
"label": sub_agent_id
|
|
172
|
+
}
|
|
173
|
+
opts = {
|
|
174
|
+
"model": {
|
|
175
|
+
"label": trans("agent.option.model"),
|
|
176
|
+
"type": "combo",
|
|
177
|
+
"use": "models",
|
|
178
|
+
"default": MODEL_DEFAULT,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if "slots" in node:
|
|
182
|
+
slots = node["slots"]
|
|
183
|
+
if "name" in slots and slots["name"]:
|
|
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
|
+
}
|
|
191
|
+
if "instruction" in slots:
|
|
192
|
+
opts["prompt"] = {
|
|
193
|
+
"type": "textarea",
|
|
194
|
+
"label": trans("agent.option.prompt"),
|
|
195
|
+
"default": slots["instruction"],
|
|
196
|
+
}
|
|
197
|
+
if "remote_tools" in slots:
|
|
198
|
+
opts["allow_remote_tools"] = {
|
|
199
|
+
"type": "bool",
|
|
200
|
+
"label": trans("agent.option.tools.remote"),
|
|
201
|
+
"description": trans("agent.option.tools.remote.desc"),
|
|
202
|
+
"default": slots["remote_tools"],
|
|
203
|
+
}
|
|
204
|
+
if "local_tools" in slots:
|
|
205
|
+
opts["allow_local_tools"] = {
|
|
206
|
+
"type": "bool",
|
|
207
|
+
"label": trans("agent.option.tools.local"),
|
|
208
|
+
"description": trans("agent.option.tools.local.desc"),
|
|
209
|
+
"default": slots["local_tools"],
|
|
210
|
+
}
|
|
211
|
+
tab["options"] = opts
|
|
212
|
+
options[sub_agent_id] = tab
|
|
213
|
+
except Exception as e:
|
|
214
|
+
self.window.core.debug.log(f"Failed to build options for custom agent '{agent_id}': {e}")
|
|
215
|
+
continue
|
|
216
|
+
return options
|
|
217
|
+
|
|
218
|
+
def new_agent(self, name: str):
|
|
219
|
+
"""
|
|
220
|
+
Create new custom agent
|
|
221
|
+
|
|
222
|
+
:param name: agent name
|
|
223
|
+
"""
|
|
224
|
+
if not self.loaded:
|
|
225
|
+
self.load()
|
|
226
|
+
new_id = str(uuid4())
|
|
227
|
+
new_agent = AgentItem()
|
|
228
|
+
new_agent.id = new_id
|
|
229
|
+
new_agent.name = name
|
|
230
|
+
self.agents[new_id] = new_agent
|
|
231
|
+
self.save()
|
|
232
|
+
return new_id
|
|
233
|
+
|
|
234
|
+
def duplicate_agent(self, agent_id: str, new_name: str):
|
|
235
|
+
"""
|
|
236
|
+
Duplicate custom agent
|
|
237
|
+
|
|
238
|
+
:param agent_id: agent ID
|
|
239
|
+
:param new_name: new agent name
|
|
240
|
+
"""
|
|
241
|
+
if not self.loaded:
|
|
242
|
+
self.load()
|
|
243
|
+
agent = self.agents.get(agent_id)
|
|
244
|
+
if agent:
|
|
245
|
+
new_agent = copy.deepcopy(agent)
|
|
246
|
+
new_agent.id = str(uuid4())
|
|
247
|
+
new_agent.name = new_name
|
|
248
|
+
self.agents[new_agent.id] = new_agent
|
|
249
|
+
self.save()
|
|
250
|
+
|
|
251
|
+
def delete_agent(self, agent_id: str):
|
|
252
|
+
"""
|
|
253
|
+
Delete custom agent
|
|
254
|
+
|
|
255
|
+
:param agent_id: agent ID
|
|
256
|
+
"""
|
|
257
|
+
if not self.loaded:
|
|
258
|
+
self.load()
|
|
259
|
+
if agent_id in self.agents:
|
|
260
|
+
del self.agents[agent_id]
|
|
261
|
+
self.save()
|
|
262
|
+
|
|
263
|
+
def update_agent(self, agent_id: str, layout: dict, schema: list):
|
|
264
|
+
"""
|
|
265
|
+
Update layout and schema of a specific custom agent
|
|
266
|
+
|
|
267
|
+
:param agent_id: agent ID
|
|
268
|
+
:param layout: dictionary with new layout
|
|
269
|
+
:param schema: list with new schema
|
|
270
|
+
"""
|
|
271
|
+
if not self.loaded:
|
|
272
|
+
self.load()
|
|
273
|
+
agent = self.agents.get(agent_id)
|
|
274
|
+
if agent:
|
|
275
|
+
if layout is None:
|
|
276
|
+
layout = {}
|
|
277
|
+
if schema is None:
|
|
278
|
+
schema = []
|
|
279
|
+
agent.layout = layout
|
|
280
|
+
agent.schema = schema
|
|
281
|
+
self.save()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# ================================================== #
|
|
4
|
+
# This file is a part of PYGPT package #
|
|
5
|
+
# Website: https://pygpt.net #
|
|
6
|
+
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
|
+
# MIT License #
|
|
8
|
+
# Created By : Marcin Szczygliński #
|
|
9
|
+
# Updated Date: 2025.09.24 23:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
from typing import Any, List, Optional
|
|
14
|
+
|
|
15
|
+
from agents import TResponseInputItem
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def ellipsize(text: str, limit: int = 280) -> str:
|
|
19
|
+
"""Shorten text for logs."""
|
|
20
|
+
if text is None:
|
|
21
|
+
return ""
|
|
22
|
+
s = str(text).replace("\n", " ").replace("\r", " ")
|
|
23
|
+
return s if len(s) <= limit else s[: max(0, limit - 3)] + "..."
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def content_to_text(content: Any) -> str:
|
|
27
|
+
"""Convert 'content' which may be str or list[dict] to plain text for logs."""
|
|
28
|
+
if isinstance(content, str):
|
|
29
|
+
return content
|
|
30
|
+
if isinstance(content, list):
|
|
31
|
+
out: List[str] = []
|
|
32
|
+
for part in content:
|
|
33
|
+
if isinstance(part, dict):
|
|
34
|
+
if "text" in part and isinstance(part["text"], str):
|
|
35
|
+
out.append(part["text"])
|
|
36
|
+
elif part.get("type") in ("output_text", "input_text") and "text" in part:
|
|
37
|
+
out.append(str(part["text"]))
|
|
38
|
+
else:
|
|
39
|
+
# fallback – best-effort stringify
|
|
40
|
+
t = part.get("text") or ""
|
|
41
|
+
out.append(str(t))
|
|
42
|
+
else:
|
|
43
|
+
out.append(str(part))
|
|
44
|
+
return " ".join(out)
|
|
45
|
+
return str(content or "")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def items_preview(items: List[TResponseInputItem], total_chars: int = 280, max_items: int = 4) -> str:
|
|
49
|
+
"""
|
|
50
|
+
Produce compact preview of last N messages with roles and truncated content.
|
|
51
|
+
"""
|
|
52
|
+
if not items:
|
|
53
|
+
return "(empty)"
|
|
54
|
+
pick = items[-max_items:]
|
|
55
|
+
per = max(32, total_chars // max(1, len(pick)))
|
|
56
|
+
lines: List[str] = []
|
|
57
|
+
for it in pick:
|
|
58
|
+
if not isinstance(it, dict):
|
|
59
|
+
lines.append(ellipsize(str(it), per))
|
|
60
|
+
continue
|
|
61
|
+
role = it.get("role", "?")
|
|
62
|
+
text = content_to_text(it.get("content"))
|
|
63
|
+
lines.append(f"- {role}: {ellipsize(text, per)}")
|
|
64
|
+
return " | ".join(lines)
|