pygpt-net 2.6.60__py3-none-any.whl → 2.6.62__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. pygpt_net/CHANGELOG.txt +14 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/chat/common.py +115 -6
  4. pygpt_net/controller/chat/input.py +4 -1
  5. pygpt_net/controller/chat/response.py +8 -2
  6. pygpt_net/controller/presets/presets.py +121 -6
  7. pygpt_net/controller/settings/editor.py +0 -15
  8. pygpt_net/controller/settings/profile.py +16 -4
  9. pygpt_net/controller/settings/workdir.py +30 -5
  10. pygpt_net/controller/theme/common.py +4 -2
  11. pygpt_net/controller/theme/markdown.py +4 -7
  12. pygpt_net/controller/theme/theme.py +2 -1
  13. pygpt_net/controller/ui/ui.py +32 -7
  14. pygpt_net/core/agents/custom/__init__.py +7 -1
  15. pygpt_net/core/agents/custom/llama_index/factory.py +17 -6
  16. pygpt_net/core/agents/custom/llama_index/runner.py +52 -4
  17. pygpt_net/core/agents/custom/llama_index/utils.py +12 -1
  18. pygpt_net/core/agents/custom/router.py +45 -6
  19. pygpt_net/core/agents/custom/runner.py +11 -5
  20. pygpt_net/core/agents/custom/schema.py +3 -1
  21. pygpt_net/core/agents/custom/utils.py +13 -1
  22. pygpt_net/core/agents/runners/llama_workflow.py +65 -5
  23. pygpt_net/core/agents/runners/openai_workflow.py +2 -1
  24. pygpt_net/core/db/viewer.py +11 -5
  25. pygpt_net/core/node_editor/graph.py +18 -9
  26. pygpt_net/core/node_editor/models.py +9 -2
  27. pygpt_net/core/node_editor/types.py +15 -1
  28. pygpt_net/core/presets/presets.py +216 -29
  29. pygpt_net/core/render/markdown/parser.py +0 -2
  30. pygpt_net/core/render/web/renderer.py +76 -11
  31. pygpt_net/data/config/config.json +5 -6
  32. pygpt_net/data/config/models.json +3 -3
  33. pygpt_net/data/config/settings.json +2 -38
  34. pygpt_net/data/css/style.dark.css +18 -0
  35. pygpt_net/data/css/style.light.css +20 -1
  36. pygpt_net/data/locale/locale.de.ini +66 -1
  37. pygpt_net/data/locale/locale.en.ini +64 -3
  38. pygpt_net/data/locale/locale.es.ini +66 -1
  39. pygpt_net/data/locale/locale.fr.ini +66 -1
  40. pygpt_net/data/locale/locale.it.ini +66 -1
  41. pygpt_net/data/locale/locale.pl.ini +67 -2
  42. pygpt_net/data/locale/locale.uk.ini +66 -1
  43. pygpt_net/data/locale/locale.zh.ini +66 -1
  44. pygpt_net/data/locale/plugin.cmd_system.en.ini +62 -66
  45. pygpt_net/item/ctx.py +23 -1
  46. pygpt_net/provider/agents/llama_index/flow_from_schema.py +2 -2
  47. pygpt_net/provider/agents/llama_index/workflow/codeact.py +9 -6
  48. pygpt_net/provider/agents/llama_index/workflow/openai.py +38 -11
  49. pygpt_net/provider/agents/llama_index/workflow/planner.py +36 -16
  50. pygpt_net/provider/agents/llama_index/workflow/supervisor.py +60 -10
  51. pygpt_net/provider/agents/openai/agent.py +3 -1
  52. pygpt_net/provider/agents/openai/agent_b2b.py +13 -9
  53. pygpt_net/provider/agents/openai/agent_planner.py +6 -2
  54. pygpt_net/provider/agents/openai/agent_with_experts.py +4 -1
  55. pygpt_net/provider/agents/openai/agent_with_experts_feedback.py +4 -2
  56. pygpt_net/provider/agents/openai/agent_with_feedback.py +4 -2
  57. pygpt_net/provider/agents/openai/evolve.py +6 -2
  58. pygpt_net/provider/agents/openai/supervisor.py +3 -1
  59. pygpt_net/provider/api/openai/agents/response.py +1 -0
  60. pygpt_net/provider/core/config/patch.py +18 -1
  61. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +0 -6
  62. pygpt_net/tools/agent_builder/tool.py +48 -26
  63. pygpt_net/tools/agent_builder/ui/dialogs.py +36 -28
  64. pygpt_net/ui/__init__.py +2 -4
  65. pygpt_net/ui/dialog/about.py +58 -38
  66. pygpt_net/ui/dialog/db.py +142 -3
  67. pygpt_net/ui/dialog/preset.py +47 -8
  68. pygpt_net/ui/layout/toolbox/presets.py +64 -16
  69. pygpt_net/ui/main.py +2 -2
  70. pygpt_net/ui/widget/dialog/confirm.py +27 -3
  71. pygpt_net/ui/widget/dialog/db.py +0 -0
  72. pygpt_net/ui/widget/draw/painter.py +90 -1
  73. pygpt_net/ui/widget/lists/preset.py +908 -60
  74. pygpt_net/ui/widget/node_editor/command.py +10 -10
  75. pygpt_net/ui/widget/node_editor/config.py +157 -0
  76. pygpt_net/ui/widget/node_editor/editor.py +223 -153
  77. pygpt_net/ui/widget/node_editor/item.py +12 -11
  78. pygpt_net/ui/widget/node_editor/node.py +246 -13
  79. pygpt_net/ui/widget/node_editor/view.py +179 -63
  80. pygpt_net/ui/widget/tabs/output.py +1 -1
  81. pygpt_net/ui/widget/textarea/input.py +157 -23
  82. pygpt_net/utils.py +114 -2
  83. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.dist-info}/METADATA +26 -100
  84. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.dist-info}/RECORD +86 -85
  85. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.dist-info}/LICENSE +0 -0
  86. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.dist-info}/WHEEL +0 -0
  87. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.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: 2024.12.14 08:00:00 #
9
+ # Updated Date: 2025.09.26 13:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -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
- if self.window.core.config.get('theme.markdown'):
38
- self.load()
39
- else:
40
- self.set_default()
36
+
37
+ self.load()
41
38
  self.apply()
42
39
 
43
40
  if force:
@@ -95,7 +92,7 @@ class Markdown:
95
92
  if base_name == 'web':
96
93
  suffix = "-" + web_style
97
94
  self.web_style = web_style
98
- theme = self.window.core.config.get('theme')
95
+ theme = str(self.window.core.config.get('theme'))
99
96
  name = str(base_name)
100
97
  if theme.startswith('light'):
101
98
  color = '.light'
@@ -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.08.27 00:00:00 #
9
+ # Updated Date: 2025.09.26 13:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -83,6 +83,7 @@ class Theme:
83
83
  :param name: theme name
84
84
  :param force: force theme change (manual trigger)
85
85
  """
86
+ self.current_theme = name
86
87
  window = self.window
87
88
  core = window.core
88
89
  controller = window.controller
@@ -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.08.23 15:00:00 #
9
+ # Updated Date: 2025.09.26 17: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
@@ -51,6 +51,9 @@ class UI:
51
51
  self._last_chat_model = None
52
52
  self._last_chat_label = None
53
53
 
54
+ # Cache for Input tab tooltip to avoid redundant updates
55
+ self._last_input_tab_tooltip = None
56
+
54
57
  def setup(self):
55
58
  """Setup UI"""
56
59
  self.update_font_size()
@@ -150,25 +153,47 @@ class UI:
150
153
  def update_tokens(self):
151
154
  """Update tokens counter in real-time"""
152
155
  ui_nodes = self.window.ui.nodes
153
- prompt = ui_nodes['input'].toPlainText().strip()
156
+
157
+ # Read raw input for accurate character count (without trimming)
158
+ raw_text = ui_nodes['input'].toPlainText()
159
+ prompt = raw_text.strip()
160
+
154
161
  input_tokens, system_tokens, extra_tokens, ctx_tokens, ctx_len, ctx_len_all, \
155
162
  sum_tokens, max_current, threshold = self.window.core.tokens.get_current(prompt)
156
163
  attachments_tokens = self.window.controller.chat.attachment.get_current_tokens()
157
164
  sum_tokens += attachments_tokens
158
165
 
159
- ctx_string = f"{ctx_len} / {ctx_len_all} - {ctx_tokens} {trans('ctx.tokens')}"
166
+ ctx_string = f"{short_num(ctx_len)} / {short_num(ctx_len_all)} - {short_num(ctx_tokens)} {trans('ctx.tokens')}"
160
167
  if ctx_string != self._last_ctx_string:
161
168
  ui_nodes['prompt.context'].setText(ctx_string)
162
169
  self._last_ctx_string = ctx_string
163
170
 
164
- parsed_sum = self.format_tokens(sum_tokens)
165
- parsed_max_current = self.format_tokens(max_current)
171
+ if max_current > 0:
172
+ max_str = short_num(max_current)
173
+ else:
174
+ max_str = "∞"
166
175
 
167
- input_string = f"{input_tokens} + {system_tokens} + {ctx_tokens} + {extra_tokens} + {attachments_tokens} = {parsed_sum} / {parsed_max_current}"
176
+ 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)} / {max_str}"
168
177
  if input_string != self._last_input_string:
169
178
  ui_nodes['input.counter'].setText(input_string)
170
179
  self._last_input_string = input_string
171
180
 
181
+ # Update Input tab tooltip with live "<chars> chars (~<tokens> tokens)" string
182
+ try:
183
+ tabs = self.window.ui.tabs.get('input')
184
+ except Exception:
185
+ tabs = None
186
+
187
+ if tabs is not None:
188
+ try:
189
+ tooltip = trans("input.tab.tooltip").format(chars=short_num(len(raw_text)), tokens=short_num(input_tokens))
190
+ except Exception:
191
+ tooltip = ""
192
+ #tooltip = f"{short_num(len(raw_text))} chars (~{short_num(input_tokens)} tokens)"
193
+ if tooltip != self._last_input_tab_tooltip:
194
+ tabs.setTabToolTip(0, tooltip)
195
+ self._last_input_tab_tooltip = tooltip
196
+
172
197
  def store_state(self):
173
198
  """Store UI state"""
174
199
  self.window.controller.layout.scroll_save()
@@ -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.24 23:00:00 #
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.24 23:00:00 #
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, 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 behave better with FunctionAgent (JSON compliance)
73
+ agent_cls = FunctionAgent # routers: keep JSON compliance
67
74
  else:
68
- agent_cls = FunctionAgent if node_tools else ReActAgent
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}, routes={allowed_routes}"
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.24 23:00:00 #
9
+ # Updated Date: 2025.09.26 17: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__}")
@@ -240,13 +268,28 @@ class DynamicFlowWorkflowLI(Workflow):
240
268
  async def _emit_header(self, ctx: Context, name: str):
241
269
  if self.dbg.event_echo:
242
270
  self.logger.debug(f"[event] header emit begin name='{name}'")
243
- await self._emit_agent_text(ctx, f"\n\n**{name}**\n\n", agent_name=name)
271
+ await self._emit_agent_text(ctx, "", agent_name=name)
272
+ # await self._emit_agent_text(ctx, f"\n\n**{name}**\n\n", agent_name=name)
244
273
  if self.dbg.event_echo:
245
274
  self.logger.debug("[event] header emit done")
246
275
 
247
276
  async def _emit_step_sep(self, ctx: Context, node_id: str):
248
277
  try:
249
- await self._emit(ctx, StepEvent(name="next", index=self._steps, total=self.max_iterations, meta={"node": node_id}))
278
+ # Include human-friendly agent name in StepEvent meta for downstream ctx propagation.
279
+ a = self.fs.agents.get(node_id)
280
+ friendly_name = (a.name if a and a.name else node_id)
281
+ await self._emit(
282
+ ctx,
283
+ StepEvent(
284
+ name="next",
285
+ index=self._steps,
286
+ total=self.max_iterations,
287
+ meta={
288
+ "node": node_id,
289
+ "agent_name": friendly_name, # pass current agent display name
290
+ },
291
+ ),
292
+ )
250
293
  except Exception as e:
251
294
  self.logger.error(f"[event] StepEvent emit failed: {e}")
252
295
 
@@ -412,6 +455,7 @@ class DynamicFlowWorkflowLI(Workflow):
412
455
  f"[runtime] model={getattr(node_rt.model,'name',str(node_rt.model))} "
413
456
  f"allow_local={node_rt.allow_local_tools} allow_remote={node_rt.allow_remote_tools} "
414
457
  f"instructions='{ellipsize(node_rt.instructions, self.dbg.preview_chars)}'"
458
+ f" role='{ellipsize(node_rt.role or '', self.dbg.preview_chars)}'"
415
459
  )
416
460
 
417
461
  llm_node = resolve_llm(self.window, node_rt.model, self.llm_base, self.stream)
@@ -430,13 +474,17 @@ class DynamicFlowWorkflowLI(Workflow):
430
474
  f"user='{ellipsize(user_msg_text, self.dbg.preview_chars)}'"
431
475
  )
432
476
 
477
+ # Prepare friendly map with optional roles for this node's allowed routes
478
+ allowed_routes_now = list(node.outputs or [])
479
+ friendly_map = self._friendly_map_for_routes(allowed_routes_now)
480
+
433
481
  # Build agent (chat_history/max_iterations in ctor – best practice)
434
482
  built = self.factory.build(
435
483
  node=node,
436
484
  node_runtime=node_rt,
437
485
  llm=llm_node,
438
486
  tools=tools_node,
439
- friendly_map=self._friendly_map(),
487
+ friendly_map=friendly_map,
440
488
  chat_history=chat_history_msgs,
441
489
  max_iterations=self.max_iterations,
442
490
  )
@@ -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.24 23:00:00 #
9
+ # Updated Date: 2025.09.25 14:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from __future__ import annotations
@@ -47,6 +47,7 @@ def make_option_getter(base_agent, preset: Optional[PresetItem]) -> OptionGetter
47
47
  class NodeRuntime:
48
48
  model: ModelItem
49
49
  instructions: str
50
+ role: Optional[str]
50
51
  allow_local_tools: bool
51
52
  allow_remote_tools: bool
52
53
 
@@ -76,6 +77,15 @@ def resolve_node_runtime(
76
77
  prompt_opt = option_get(node.id, "prompt", None)
77
78
  instructions = (prompt_opt or getattr(node, "instruction", None) or base_prompt or "").strip()
78
79
 
80
+ # Role resolve (optional)
81
+ role_opt = option_get(node.id, "role", None)
82
+ role_from_schema = getattr(node, "role", None) if hasattr(node, "role") else None
83
+ role: Optional[str] = None
84
+ if isinstance(role_opt, str) and role_opt.strip():
85
+ role = role_opt.strip()
86
+ elif isinstance(role_from_schema, str) and role_from_schema.strip():
87
+ role = role_from_schema.strip()
88
+
79
89
  allow_local_tools = bool(
80
90
  option_get(
81
91
  node.id, "allow_local_tools",
@@ -92,6 +102,7 @@ def resolve_node_runtime(
92
102
  return NodeRuntime(
93
103
  model=model_item,
94
104
  instructions=instructions,
105
+ role=role,
95
106
  allow_local_tools=allow_local_tools,
96
107
  allow_remote_tools=allow_remote_tools,
97
108
  )
@@ -6,14 +6,14 @@
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.24 23:00:00 #
9
+ # Updated Date: 2025.09.25 14:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from __future__ import annotations
13
13
  import json
14
14
  import re
15
15
  from dataclasses import dataclass
16
- from typing import List, Optional, Tuple, Any
16
+ from typing import List, Optional, Tuple, Any, Dict
17
17
 
18
18
 
19
19
  @dataclass
@@ -25,13 +25,46 @@ class RouteDecision:
25
25
  error: Optional[str] = None
26
26
 
27
27
 
28
- def build_router_instruction(agent_name: str, current_id: str, allowed_routes: List[str], friendly_map: dict[str, str]) -> str:
28
+ def build_router_instruction(
29
+ agent_name: str,
30
+ current_id: str,
31
+ allowed_routes: List[str],
32
+ friendly_map: Dict[str, Any],
33
+ ) -> str:
29
34
  """
30
35
  Builds an instruction that forces the model to output JSON with next route and content.
36
+
37
+ Additionally, if the provided friendly_map contains role information for any of the
38
+ allowed routes (e.g. friendly_map[id] is a dict with a "role" key), these roles
39
+ are included in the instruction to improve routing. This is optional and included
40
+ only when present and non-empty.
31
41
  """
32
42
  allowed = ", ".join(allowed_routes)
33
- friendly = {rid: friendly_map.get(rid, rid) for rid in allowed_routes}
34
- return (
43
+
44
+ # Normalize human-friendly names: accept either a plain string or a dict with
45
+ # common name keys such as "name", "title" or "label".
46
+ def _extract_name(val: Any, default_name: str) -> str:
47
+ if isinstance(val, str):
48
+ return val
49
+ if isinstance(val, dict):
50
+ return str(val.get("name") or val.get("title") or val.get("label") or default_name)
51
+ return default_name
52
+
53
+ # Extract names and optional roles for allowed routes, without changing input map semantics.
54
+ friendly_names = {rid: _extract_name(friendly_map.get(rid), rid) for rid in allowed_routes}
55
+
56
+ # Roles are optional; include only non-empty strings.
57
+ friendly_roles: Dict[str, str] = {}
58
+ for rid in allowed_routes:
59
+ val = friendly_map.get(rid)
60
+ role_val: Optional[str] = None
61
+ if isinstance(val, dict):
62
+ role_val = val.get("role")
63
+ if isinstance(role_val, str) and role_val.strip():
64
+ friendly_roles[rid] = role_val.strip()
65
+
66
+ # Base instruction
67
+ instr = (
35
68
  "You are a routing-capable agent in a multi-agent flow.\n"
36
69
  f"Your id is: {current_id}, name: {agent_name}.\n"
37
70
  "You MUST respond ONLY with a single JSON object and nothing else.\n"
@@ -46,9 +79,15 @@ def build_router_instruction(agent_name: str, current_id: str, allowed_routes: L
46
79
  "- content must contain the user-facing answer (you may include structured data as JSON or Markdown inside content).\n"
47
80
  "- Do NOT add any commentary outside of the JSON. No leading or trailing text.\n"
48
81
  "- If using tools, still return the final JSON with tool results summarized in content.\n"
49
- f"- Human-friendly route names: {json.dumps(friendly)}\n"
82
+ f"- Human-friendly route names: {json.dumps(friendly_names)}\n"
50
83
  )
51
84
 
85
+ # Append roles only if any are present (optional).
86
+ if friendly_roles:
87
+ instr += f"- Human-friendly route roles (optional): {json.dumps(friendly_roles)}\n"
88
+
89
+ return instr
90
+
52
91
 
53
92
  def _extract_json_block(text: str) -> Optional[str]:
54
93
  """
@@ -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.24 23:00:00 #
9
+ # Updated Date: 2025.09.26 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from __future__ import annotations
@@ -171,6 +171,7 @@ class FlowOrchestrator:
171
171
  f"[runtime] model={getattr(node_rt.model,'name',str(node_rt.model))} "
172
172
  f"allow_local={node_rt.allow_local_tools} allow_remote={node_rt.allow_remote_tools} "
173
173
  f"instructions='{instr_preview}'"
174
+ f" role='{node_rt.role}'"
174
175
  )
175
176
 
176
177
  # Memory selection and INPUT BUILD (memory-first policy)
@@ -241,13 +242,14 @@ class FlowOrchestrator:
241
242
  run_kwargs["trace_id"] = trace_id
242
243
 
243
244
  # Header for UI
244
- title = f"\n\n**{built.name}**\n\n"
245
- ctx.stream = title
245
+ ctx.set_agent_name(agent.name)
246
+ # title = f"\n\n**{built.name}**\n\n"
247
+ # ctx.stream = title
246
248
  bridge.on_step(ctx, begin)
247
249
  begin = False
248
250
  handler.begin = begin
249
- if not use_partial_ctx:
250
- handler.to_buffer(title)
251
+ # if not use_partial_ctx:
252
+ # handler.to_buffer(title)
251
253
 
252
254
  display_text = "" # what we show to UI for this step
253
255
  next_id: Optional[str] = None
@@ -443,6 +445,10 @@ class FlowOrchestrator:
443
445
  else:
444
446
  bridge.on_next(ctx)
445
447
 
448
+ # set next agent name if not at the end
449
+ if current_ids and current_ids[0] in fs.agents:
450
+ ctx.set_agent_name(fs.agents[current_ids[0]].name)
451
+
446
452
  # Step duration
447
453
  dur = perf_counter() - step_start
448
454
  self.logger.debug(f"[step {steps}] duration={dur:.3f}s")
@@ -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.24 23:00:00 #
9
+ # Updated Date: 2025.09.25 14:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from __future__ import annotations
@@ -31,6 +31,7 @@ class AgentNode(BaseNode):
31
31
  inputs: List[str] = field(default_factory=list)
32
32
  memory_out: Optional[str] = None # single mem by spec
33
33
  memory_in: List[str] = field(default_factory=list) # not used, but kept for completeness
34
+ role: str = "" # Optional short description of agent's purpose
34
35
 
35
36
 
36
37
  @dataclass
@@ -91,6 +92,7 @@ def parse_schema(schema: List[Dict[str, Any]]) -> FlowSchema:
91
92
  inputs=list(_safe_get(slots, "input", "in", default=[])) or [],
92
93
  memory_out=(_safe_get(slots, "memory", "out", default=[None]) or [None])[0],
93
94
  memory_in=list(_safe_get(slots, "memory", "in", default=[])) or [],
95
+ role=_safe_get(slots, "role", default="") or "",
94
96
  )
95
97
  fs.agents[nid] = node
96
98
 
@@ -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.24 23:00:00 #
9
+ # Updated Date: 2025.09.25 14:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from __future__ import annotations
@@ -116,6 +116,7 @@ def make_option_getter(base_agent, preset: Optional[PresetItem]) -> OptionGetter
116
116
  class NodeRuntime:
117
117
  model: ModelItem
118
118
  instructions: str
119
+ role: Optional[str]
119
120
  allow_local_tools: bool
120
121
  allow_remote_tools: bool
121
122
 
@@ -138,6 +139,7 @@ def resolve_node_runtime(
138
139
  Priority:
139
140
  - model: get_option(node.id, "model") -> window.core.models.get(name) -> default_model
140
141
  - prompt: get_option(node.id, "prompt") -> node.instruction -> base_prompt -> ""
142
+ - role: get_option(node.id, "role") -> node.role -> None (only used if provided and non-empty)
141
143
  - allow_*: get_option(node.id, "allow_local_tools"/"allow_remote_tools")
142
144
  -> schema flags -> defaults
143
145
  """
@@ -157,6 +159,15 @@ def resolve_node_runtime(
157
159
  prompt_opt = option_get(node.id, "prompt", None)
158
160
  instructions = (prompt_opt or getattr(node, "instruction", None) or base_prompt or "").strip()
159
161
 
162
+ # Role resolve (optional)
163
+ role_opt = option_get(node.id, "role", None)
164
+ role_from_schema = getattr(node, "role", None) if hasattr(node, "role") else None
165
+ role: Optional[str] = None
166
+ if isinstance(role_opt, str) and role_opt.strip():
167
+ role = role_opt.strip()
168
+ elif isinstance(role_from_schema, str) and role_from_schema.strip():
169
+ role = role_from_schema.strip()
170
+
160
171
  # Tools flags resolve
161
172
  allow_local_tools = bool(
162
173
  option_get(
@@ -176,6 +187,7 @@ def resolve_node_runtime(
176
187
  return NodeRuntime(
177
188
  model=model_item,
178
189
  instructions=instructions,
190
+ role=role,
179
191
  allow_local_tools=allow_local_tools,
180
192
  allow_remote_tools=allow_remote_tools,
181
193
  )