pygpt-net 2.6.58__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.
Files changed (72) hide show
  1. pygpt_net/CHANGELOG.txt +10 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +9 -5
  4. pygpt_net/controller/__init__.py +1 -0
  5. pygpt_net/controller/presets/editor.py +442 -39
  6. pygpt_net/core/agents/custom/__init__.py +275 -0
  7. pygpt_net/core/agents/custom/debug.py +64 -0
  8. pygpt_net/core/agents/custom/factory.py +109 -0
  9. pygpt_net/core/agents/custom/graph.py +71 -0
  10. pygpt_net/core/agents/custom/llama_index/__init__.py +10 -0
  11. pygpt_net/core/agents/custom/llama_index/factory.py +89 -0
  12. pygpt_net/core/agents/custom/llama_index/router_streamer.py +106 -0
  13. pygpt_net/core/agents/custom/llama_index/runner.py +529 -0
  14. pygpt_net/core/agents/custom/llama_index/stream.py +56 -0
  15. pygpt_net/core/agents/custom/llama_index/utils.py +242 -0
  16. pygpt_net/core/agents/custom/logging.py +50 -0
  17. pygpt_net/core/agents/custom/memory.py +51 -0
  18. pygpt_net/core/agents/custom/router.py +116 -0
  19. pygpt_net/core/agents/custom/router_streamer.py +187 -0
  20. pygpt_net/core/agents/custom/runner.py +454 -0
  21. pygpt_net/core/agents/custom/schema.py +125 -0
  22. pygpt_net/core/agents/custom/utils.py +181 -0
  23. pygpt_net/core/agents/provider.py +72 -7
  24. pygpt_net/core/agents/runner.py +7 -4
  25. pygpt_net/core/agents/runners/helpers.py +1 -1
  26. pygpt_net/core/agents/runners/llama_workflow.py +3 -0
  27. pygpt_net/core/agents/runners/openai_workflow.py +8 -1
  28. pygpt_net/core/filesystem/parser.py +37 -24
  29. pygpt_net/{ui/widget/builder → core/node_editor}/__init__.py +2 -2
  30. pygpt_net/core/{builder → node_editor}/graph.py +11 -218
  31. pygpt_net/core/node_editor/models.py +111 -0
  32. pygpt_net/core/node_editor/types.py +76 -0
  33. pygpt_net/core/node_editor/utils.py +17 -0
  34. pygpt_net/core/render/web/renderer.py +10 -8
  35. pygpt_net/data/config/config.json +3 -3
  36. pygpt_net/data/config/models.json +3 -3
  37. pygpt_net/data/locale/locale.en.ini +4 -4
  38. pygpt_net/data/locale/plugin.cmd_system.en.ini +68 -0
  39. pygpt_net/item/agent.py +5 -1
  40. pygpt_net/item/preset.py +19 -1
  41. pygpt_net/plugin/cmd_system/config.py +377 -1
  42. pygpt_net/plugin/cmd_system/plugin.py +52 -8
  43. pygpt_net/plugin/cmd_system/runner.py +508 -32
  44. pygpt_net/plugin/cmd_system/winapi.py +481 -0
  45. pygpt_net/plugin/cmd_system/worker.py +88 -15
  46. pygpt_net/provider/agents/base.py +33 -2
  47. pygpt_net/provider/agents/llama_index/flow_from_schema.py +92 -0
  48. pygpt_net/provider/agents/llama_index/workflow/supervisor.py +0 -0
  49. pygpt_net/provider/agents/openai/flow_from_schema.py +96 -0
  50. pygpt_net/provider/core/agent/json_file.py +11 -5
  51. pygpt_net/provider/llms/openai.py +6 -4
  52. pygpt_net/tools/agent_builder/tool.py +217 -52
  53. pygpt_net/tools/agent_builder/ui/dialogs.py +119 -24
  54. pygpt_net/tools/agent_builder/ui/list.py +37 -10
  55. pygpt_net/tools/code_interpreter/ui/html.py +2 -1
  56. pygpt_net/ui/dialog/preset.py +16 -1
  57. pygpt_net/ui/main.py +1 -1
  58. pygpt_net/{core/builder → ui/widget/node_editor}/__init__.py +2 -2
  59. pygpt_net/ui/widget/node_editor/command.py +373 -0
  60. pygpt_net/ui/widget/node_editor/editor.py +2038 -0
  61. pygpt_net/ui/widget/node_editor/item.py +492 -0
  62. pygpt_net/ui/widget/node_editor/node.py +1205 -0
  63. pygpt_net/ui/widget/node_editor/utils.py +17 -0
  64. pygpt_net/ui/widget/node_editor/view.py +247 -0
  65. pygpt_net/ui/widget/textarea/web.py +1 -1
  66. {pygpt_net-2.6.58.dist-info → pygpt_net-2.6.60.dist-info}/METADATA +135 -61
  67. {pygpt_net-2.6.58.dist-info → pygpt_net-2.6.60.dist-info}/RECORD +69 -42
  68. pygpt_net/core/agents/custom.py +0 -150
  69. pygpt_net/ui/widget/builder/editor.py +0 -2001
  70. {pygpt_net-2.6.58.dist-info → pygpt_net-2.6.60.dist-info}/LICENSE +0 -0
  71. {pygpt_net-2.6.58.dist-info → pygpt_net-2.6.60.dist-info}/WHEEL +0 -0
  72. {pygpt_net-2.6.58.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
+ )