pygpt-net 2.6.59__py3-none-any.whl → 2.6.60__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 +4 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +9 -5
- pygpt_net/controller/__init__.py +1 -0
- pygpt_net/controller/presets/editor.py +442 -39
- pygpt_net/core/agents/custom/__init__.py +275 -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 +89 -0
- pygpt_net/core/agents/custom/llama_index/router_streamer.py +106 -0
- pygpt_net/core/agents/custom/llama_index/runner.py +529 -0
- pygpt_net/core/agents/custom/llama_index/stream.py +56 -0
- pygpt_net/core/agents/custom/llama_index/utils.py +242 -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 +116 -0
- pygpt_net/core/agents/custom/router_streamer.py +187 -0
- pygpt_net/core/agents/custom/runner.py +454 -0
- pygpt_net/core/agents/custom/schema.py +125 -0
- pygpt_net/core/agents/custom/utils.py +181 -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/{ui/widget/builder → core/node_editor}/__init__.py +2 -2
- pygpt_net/core/{builder → node_editor}/graph.py +11 -218
- pygpt_net/core/node_editor/models.py +111 -0
- pygpt_net/core/node_editor/types.py +76 -0
- pygpt_net/core/node_editor/utils.py +17 -0
- pygpt_net/core/render/web/renderer.py +10 -8
- pygpt_net/data/config/config.json +3 -3
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/locale/locale.en.ini +4 -4
- 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/tools/agent_builder/tool.py +217 -52
- pygpt_net/tools/agent_builder/ui/dialogs.py +119 -24
- pygpt_net/tools/agent_builder/ui/list.py +37 -10
- pygpt_net/ui/dialog/preset.py +16 -1
- pygpt_net/ui/main.py +1 -1
- 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/editor.py +2038 -0
- pygpt_net/ui/widget/node_editor/item.py +492 -0
- pygpt_net/ui/widget/node_editor/node.py +1205 -0
- pygpt_net/ui/widget/node_editor/utils.py +17 -0
- pygpt_net/ui/widget/node_editor/view.py +247 -0
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.60.dist-info}/METADATA +72 -2
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.60.dist-info}/RECORD +59 -33
- 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.60.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.60.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.60.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,275 @@
|
|
|
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
|
+
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 "instruction" in slots:
|
|
186
|
+
opts["prompt"] = {
|
|
187
|
+
"type": "textarea",
|
|
188
|
+
"label": trans("agent.option.prompt"),
|
|
189
|
+
"default": slots["instruction"],
|
|
190
|
+
}
|
|
191
|
+
if "remote_tools" in slots:
|
|
192
|
+
opts["allow_remote_tools"] = {
|
|
193
|
+
"type": "bool",
|
|
194
|
+
"label": trans("agent.option.tools.remote"),
|
|
195
|
+
"description": trans("agent.option.tools.remote.desc"),
|
|
196
|
+
"default": slots["remote_tools"],
|
|
197
|
+
}
|
|
198
|
+
if "local_tools" in slots:
|
|
199
|
+
opts["allow_local_tools"] = {
|
|
200
|
+
"type": "bool",
|
|
201
|
+
"label": trans("agent.option.tools.local"),
|
|
202
|
+
"description": trans("agent.option.tools.local.desc"),
|
|
203
|
+
"default": slots["local_tools"],
|
|
204
|
+
}
|
|
205
|
+
tab["options"] = opts
|
|
206
|
+
options[sub_agent_id] = tab
|
|
207
|
+
except Exception as e:
|
|
208
|
+
self.window.core.debug.log(f"Failed to build options for custom agent '{agent_id}': {e}")
|
|
209
|
+
continue
|
|
210
|
+
return options
|
|
211
|
+
|
|
212
|
+
def new_agent(self, name: str):
|
|
213
|
+
"""
|
|
214
|
+
Create new custom agent
|
|
215
|
+
|
|
216
|
+
:param name: agent name
|
|
217
|
+
"""
|
|
218
|
+
if not self.loaded:
|
|
219
|
+
self.load()
|
|
220
|
+
new_id = str(uuid4())
|
|
221
|
+
new_agent = AgentItem()
|
|
222
|
+
new_agent.id = new_id
|
|
223
|
+
new_agent.name = name
|
|
224
|
+
self.agents[new_id] = new_agent
|
|
225
|
+
self.save()
|
|
226
|
+
return new_id
|
|
227
|
+
|
|
228
|
+
def duplicate_agent(self, agent_id: str, new_name: str):
|
|
229
|
+
"""
|
|
230
|
+
Duplicate custom agent
|
|
231
|
+
|
|
232
|
+
:param agent_id: agent ID
|
|
233
|
+
:param new_name: new agent name
|
|
234
|
+
"""
|
|
235
|
+
if not self.loaded:
|
|
236
|
+
self.load()
|
|
237
|
+
agent = self.agents.get(agent_id)
|
|
238
|
+
if agent:
|
|
239
|
+
new_agent = copy.deepcopy(agent)
|
|
240
|
+
new_agent.id = str(uuid4())
|
|
241
|
+
new_agent.name = new_name
|
|
242
|
+
self.agents[new_agent.id] = new_agent
|
|
243
|
+
self.save()
|
|
244
|
+
|
|
245
|
+
def delete_agent(self, agent_id: str):
|
|
246
|
+
"""
|
|
247
|
+
Delete custom agent
|
|
248
|
+
|
|
249
|
+
:param agent_id: agent ID
|
|
250
|
+
"""
|
|
251
|
+
if not self.loaded:
|
|
252
|
+
self.load()
|
|
253
|
+
if agent_id in self.agents:
|
|
254
|
+
del self.agents[agent_id]
|
|
255
|
+
self.save()
|
|
256
|
+
|
|
257
|
+
def update_agent(self, agent_id: str, layout: dict, schema: list):
|
|
258
|
+
"""
|
|
259
|
+
Update layout and schema of a specific custom agent
|
|
260
|
+
|
|
261
|
+
:param agent_id: agent ID
|
|
262
|
+
:param layout: dictionary with new layout
|
|
263
|
+
:param schema: list with new schema
|
|
264
|
+
"""
|
|
265
|
+
if not self.loaded:
|
|
266
|
+
self.load()
|
|
267
|
+
agent = self.agents.get(agent_id)
|
|
268
|
+
if agent:
|
|
269
|
+
if layout is None:
|
|
270
|
+
layout = {}
|
|
271
|
+
if schema is None:
|
|
272
|
+
schema = []
|
|
273
|
+
agent.layout = layout
|
|
274
|
+
agent.schema = schema
|
|
275
|
+
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)
|
|
@@ -0,0 +1,109 @@
|
|
|
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 dataclasses import dataclass
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
from agents import Agent as OpenAIAgent
|
|
17
|
+
from pygpt_net.item.preset import PresetItem
|
|
18
|
+
from pygpt_net.core.bridge import BridgeContext
|
|
19
|
+
from pygpt_net.provider.api.openai.agents.remote_tools import append_tools
|
|
20
|
+
from pygpt_net.provider.api.openai.agents.experts import get_experts
|
|
21
|
+
|
|
22
|
+
from .schema import AgentNode
|
|
23
|
+
from .router import build_router_instruction
|
|
24
|
+
from .utils import NodeRuntime
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class BuiltAgent:
|
|
29
|
+
instance: OpenAIAgent
|
|
30
|
+
name: str
|
|
31
|
+
instructions: str
|
|
32
|
+
multi_output: bool
|
|
33
|
+
allowed_routes: List[str]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AgentFactory:
|
|
37
|
+
"""
|
|
38
|
+
Builds OpenAIAgent instances from AgentNode + NodeRuntime.
|
|
39
|
+
"""
|
|
40
|
+
def __init__(self, window, logger) -> None:
|
|
41
|
+
self.window = window
|
|
42
|
+
self.logger = logger
|
|
43
|
+
|
|
44
|
+
def build(
|
|
45
|
+
self,
|
|
46
|
+
node: AgentNode,
|
|
47
|
+
node_runtime: NodeRuntime,
|
|
48
|
+
preset: Optional[PresetItem],
|
|
49
|
+
function_tools: List[dict],
|
|
50
|
+
force_router: bool,
|
|
51
|
+
friendly_map: Dict[str, str],
|
|
52
|
+
handoffs_enabled: bool = True,
|
|
53
|
+
context: Optional[BridgeContext] = None,
|
|
54
|
+
) -> BuiltAgent:
|
|
55
|
+
# Agent name
|
|
56
|
+
agent_name = (node.name or "").strip() or (preset.name if preset else f"Agent {node.id}")
|
|
57
|
+
|
|
58
|
+
# Multi-output routing instruction injection
|
|
59
|
+
multi_output = force_router or (len(node.outputs or []) > 1)
|
|
60
|
+
allowed_routes = list(node.outputs or [])
|
|
61
|
+
|
|
62
|
+
instr = node_runtime.instructions
|
|
63
|
+
if multi_output and allowed_routes:
|
|
64
|
+
router_instr = build_router_instruction(agent_name, node.id, allowed_routes, friendly_map)
|
|
65
|
+
instr = router_instr + "\n\n" + instr if instr else router_instr
|
|
66
|
+
|
|
67
|
+
# Base kwargs
|
|
68
|
+
kwargs: Dict[str, Any] = {
|
|
69
|
+
"name": agent_name,
|
|
70
|
+
"instructions": instr,
|
|
71
|
+
"model": self.window.core.agents.provider.get_openai_model(node_runtime.model),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Tools
|
|
75
|
+
tool_kwargs = append_tools(
|
|
76
|
+
tools=function_tools or [],
|
|
77
|
+
window=self.window,
|
|
78
|
+
model=node_runtime.model,
|
|
79
|
+
preset=preset,
|
|
80
|
+
allow_local_tools=node_runtime.allow_local_tools,
|
|
81
|
+
allow_remote_tools=node_runtime.allow_remote_tools,
|
|
82
|
+
)
|
|
83
|
+
kwargs.update(tool_kwargs)
|
|
84
|
+
|
|
85
|
+
# Experts/handoffs if any
|
|
86
|
+
if handoffs_enabled:
|
|
87
|
+
experts = get_experts(
|
|
88
|
+
window=self.window,
|
|
89
|
+
preset=preset,
|
|
90
|
+
verbose=False,
|
|
91
|
+
tools=function_tools or [],
|
|
92
|
+
)
|
|
93
|
+
if experts:
|
|
94
|
+
kwargs["handoffs"] = experts
|
|
95
|
+
|
|
96
|
+
# Build instance
|
|
97
|
+
instance = OpenAIAgent(**kwargs)
|
|
98
|
+
self.logger.debug(
|
|
99
|
+
f"Built agent {node.id} ({agent_name}), "
|
|
100
|
+
f"multi_output={multi_output}, routes={allowed_routes}, model={node_runtime.model.name}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return BuiltAgent(
|
|
104
|
+
instance=instance,
|
|
105
|
+
name=agent_name,
|
|
106
|
+
instructions=instr,
|
|
107
|
+
multi_output=multi_output,
|
|
108
|
+
allowed_routes=allowed_routes,
|
|
109
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
import re
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Dict, List, Optional, Tuple
|
|
16
|
+
|
|
17
|
+
from .schema import FlowSchema, AgentNode, StartNode, EndNode, MemoryNode
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class FlowGraph:
|
|
22
|
+
schema: FlowSchema
|
|
23
|
+
adjacency: Dict[str, List[str]] = field(default_factory=dict) # node_id -> list of node_ids
|
|
24
|
+
agent_to_memory: Dict[str, Optional[str]] = field(default_factory=dict) # agent_id -> mem_id or None
|
|
25
|
+
start_targets: List[str] = field(default_factory=list) # immediate next nodes from start
|
|
26
|
+
end_nodes: List[str] = field(default_factory=list) # ids of end nodes
|
|
27
|
+
|
|
28
|
+
def get_next(self, node_id: str) -> List[str]:
|
|
29
|
+
return self.adjacency.get(node_id, [])
|
|
30
|
+
|
|
31
|
+
def first_connected_end(self, node_id: str) -> Optional[str]:
|
|
32
|
+
outs = self.get_next(node_id)
|
|
33
|
+
for out in outs:
|
|
34
|
+
if out in self.schema.ends:
|
|
35
|
+
return out
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
def pick_default_start_agent(self) -> Optional[str]:
|
|
39
|
+
"""Pick lowest numeric agent id if no start is present."""
|
|
40
|
+
if not self.schema.agents:
|
|
41
|
+
return None
|
|
42
|
+
# Prefer numeric suffix; fallback to lexicographic
|
|
43
|
+
def key_fn(aid: str) -> Tuple[int, str]:
|
|
44
|
+
m = re.search(r"(\d+)$", aid)
|
|
45
|
+
if m:
|
|
46
|
+
try:
|
|
47
|
+
return (int(m.group(1)), aid)
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
return (10**9, aid)
|
|
51
|
+
|
|
52
|
+
return sorted(self.schema.agents.keys(), key=key_fn)[0]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build_graph(fs: FlowSchema) -> FlowGraph:
|
|
56
|
+
g = FlowGraph(schema=fs)
|
|
57
|
+
# adjacency from agents
|
|
58
|
+
for aid, anode in fs.agents.items():
|
|
59
|
+
g.adjacency[aid] = list(anode.outputs or [])
|
|
60
|
+
g.agent_to_memory[aid] = anode.memory_out
|
|
61
|
+
|
|
62
|
+
# adjacency from start nodes
|
|
63
|
+
g.end_nodes = list(fs.ends.keys())
|
|
64
|
+
g.start_targets = []
|
|
65
|
+
if fs.starts:
|
|
66
|
+
# By spec there can be multiple start nodes; we concatenate their outputs in order found
|
|
67
|
+
for sid, snode in fs.starts.items():
|
|
68
|
+
g.adjacency[sid] = list(snode.outputs or [])
|
|
69
|
+
g.start_targets.extend(snode.outputs or [])
|
|
70
|
+
|
|
71
|
+
return g
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
# ================================================== #
|
|
@@ -0,0 +1,89 @@
|
|
|
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 dataclasses import dataclass
|
|
14
|
+
from typing import Any, Dict, List
|
|
15
|
+
|
|
16
|
+
from llama_index.core.agent.workflow import ReActAgent, FunctionAgent
|
|
17
|
+
|
|
18
|
+
from ..schema import AgentNode
|
|
19
|
+
from ..router import build_router_instruction
|
|
20
|
+
from .utils import NodeRuntime, coerce_li_tools
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class BuiltAgentLI:
|
|
25
|
+
instance: Any
|
|
26
|
+
name: str
|
|
27
|
+
instructions: str
|
|
28
|
+
multi_output: bool
|
|
29
|
+
allowed_routes: List[str]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AgentFactoryLI:
|
|
33
|
+
"""
|
|
34
|
+
Build LlamaIndex ReActAgent/FunctionAgent from AgentNode + NodeRuntime and explicit LLM/tools.
|
|
35
|
+
Best practice: chat_history/max_iterations przekazujemy do konstruktora agenta.
|
|
36
|
+
"""
|
|
37
|
+
def __init__(self, window, logger) -> None:
|
|
38
|
+
self.window = window
|
|
39
|
+
self.logger = logger
|
|
40
|
+
|
|
41
|
+
def build(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
node: AgentNode,
|
|
45
|
+
node_runtime: NodeRuntime,
|
|
46
|
+
llm: Any, # LLM instance (z appki lub resolve_llm)
|
|
47
|
+
tools: List[Any], # BaseTool list
|
|
48
|
+
friendly_map: Dict[str, str],
|
|
49
|
+
force_router: bool = False,
|
|
50
|
+
chat_history: List[Any] = None,
|
|
51
|
+
max_iterations: int = 10,
|
|
52
|
+
) -> BuiltAgentLI:
|
|
53
|
+
agent_name = (node.name or "").strip() or f"Agent {node.id}"
|
|
54
|
+
|
|
55
|
+
multi_output = force_router or (len(node.outputs or []) > 1)
|
|
56
|
+
allowed_routes = list(node.outputs or [])
|
|
57
|
+
|
|
58
|
+
instr = node_runtime.instructions
|
|
59
|
+
if multi_output and allowed_routes:
|
|
60
|
+
router_instr = build_router_instruction(agent_name, node.id, allowed_routes, friendly_map)
|
|
61
|
+
instr = router_instr + "\n\n" + instr if instr else router_instr
|
|
62
|
+
|
|
63
|
+
node_tools = tools if (node_runtime.allow_local_tools or node_runtime.allow_remote_tools) else []
|
|
64
|
+
|
|
65
|
+
if multi_output:
|
|
66
|
+
agent_cls = FunctionAgent # routers behave better with FunctionAgent (JSON compliance)
|
|
67
|
+
else:
|
|
68
|
+
agent_cls = FunctionAgent if node_tools else ReActAgent
|
|
69
|
+
kwargs: Dict[str, Any] = {
|
|
70
|
+
"name": agent_name,
|
|
71
|
+
"system_prompt": instr,
|
|
72
|
+
"llm": llm,
|
|
73
|
+
"chat_history": chat_history or [],
|
|
74
|
+
"max_iterations": int(max_iterations),
|
|
75
|
+
}
|
|
76
|
+
if node_tools:
|
|
77
|
+
kwargs["tools"] = coerce_li_tools(node_tools)
|
|
78
|
+
|
|
79
|
+
instance = agent_cls(**kwargs)
|
|
80
|
+
self.logger.debug(
|
|
81
|
+
f"[li] Built agent {node.id} ({agent_name}), multi_output={multi_output}, routes={allowed_routes}"
|
|
82
|
+
)
|
|
83
|
+
return BuiltAgentLI(
|
|
84
|
+
instance=instance,
|
|
85
|
+
name=agent_name,
|
|
86
|
+
instructions=instr,
|
|
87
|
+
multi_output=multi_output,
|
|
88
|
+
allowed_routes=allowed_routes,
|
|
89
|
+
)
|