pygpt-net 2.6.61__py3-none-any.whl → 2.6.63__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 (86) hide show
  1. pygpt_net/CHANGELOG.txt +12 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/chat/response.py +8 -2
  4. pygpt_net/controller/presets/editor.py +65 -1
  5. pygpt_net/controller/settings/profile.py +16 -4
  6. pygpt_net/controller/settings/workdir.py +30 -5
  7. pygpt_net/controller/theme/common.py +4 -2
  8. pygpt_net/controller/theme/markdown.py +2 -2
  9. pygpt_net/controller/theme/theme.py +2 -1
  10. pygpt_net/controller/ui/ui.py +31 -3
  11. pygpt_net/core/agents/custom/llama_index/runner.py +30 -52
  12. pygpt_net/core/agents/custom/runner.py +199 -76
  13. pygpt_net/core/agents/runners/llama_workflow.py +122 -12
  14. pygpt_net/core/agents/runners/openai_workflow.py +2 -1
  15. pygpt_net/core/node_editor/types.py +13 -1
  16. pygpt_net/core/render/web/renderer.py +76 -11
  17. pygpt_net/data/config/config.json +3 -3
  18. pygpt_net/data/config/models.json +3 -3
  19. pygpt_net/data/config/presets/agent_openai_b2b.json +1 -15
  20. pygpt_net/data/config/presets/agent_openai_coder.json +1 -15
  21. pygpt_net/data/config/presets/agent_openai_evolve.json +1 -23
  22. pygpt_net/data/config/presets/agent_openai_planner.json +1 -21
  23. pygpt_net/data/config/presets/agent_openai_researcher.json +1 -21
  24. pygpt_net/data/config/presets/agent_openai_supervisor.json +1 -13
  25. pygpt_net/data/config/presets/agent_openai_writer.json +1 -15
  26. pygpt_net/data/config/presets/agent_supervisor.json +1 -11
  27. pygpt_net/data/css/style.dark.css +18 -0
  28. pygpt_net/data/css/style.light.css +20 -1
  29. pygpt_net/data/js/app/runtime.js +4 -1
  30. pygpt_net/data/js/app.min.js +3 -2
  31. pygpt_net/data/locale/locale.de.ini +2 -0
  32. pygpt_net/data/locale/locale.en.ini +7 -0
  33. pygpt_net/data/locale/locale.es.ini +2 -0
  34. pygpt_net/data/locale/locale.fr.ini +2 -0
  35. pygpt_net/data/locale/locale.it.ini +2 -0
  36. pygpt_net/data/locale/locale.pl.ini +3 -1
  37. pygpt_net/data/locale/locale.uk.ini +2 -0
  38. pygpt_net/data/locale/locale.zh.ini +2 -0
  39. pygpt_net/item/ctx.py +23 -1
  40. pygpt_net/js_rc.py +13 -10
  41. pygpt_net/provider/agents/base.py +0 -0
  42. pygpt_net/provider/agents/llama_index/flow_from_schema.py +0 -0
  43. pygpt_net/provider/agents/llama_index/workflow/codeact.py +9 -6
  44. pygpt_net/provider/agents/llama_index/workflow/openai.py +38 -11
  45. pygpt_net/provider/agents/llama_index/workflow/planner.py +248 -28
  46. pygpt_net/provider/agents/llama_index/workflow/supervisor.py +60 -10
  47. pygpt_net/provider/agents/openai/agent.py +3 -1
  48. pygpt_net/provider/agents/openai/agent_b2b.py +17 -13
  49. pygpt_net/provider/agents/openai/agent_planner.py +617 -258
  50. pygpt_net/provider/agents/openai/agent_with_experts.py +4 -1
  51. pygpt_net/provider/agents/openai/agent_with_experts_feedback.py +8 -6
  52. pygpt_net/provider/agents/openai/agent_with_feedback.py +8 -6
  53. pygpt_net/provider/agents/openai/evolve.py +12 -8
  54. pygpt_net/provider/agents/openai/flow_from_schema.py +0 -0
  55. pygpt_net/provider/agents/openai/supervisor.py +292 -37
  56. pygpt_net/provider/api/openai/agents/response.py +1 -0
  57. pygpt_net/provider/api/x_ai/__init__.py +0 -0
  58. pygpt_net/provider/core/agent/__init__.py +0 -0
  59. pygpt_net/provider/core/agent/base.py +0 -0
  60. pygpt_net/provider/core/agent/json_file.py +0 -0
  61. pygpt_net/provider/core/config/patch.py +8 -0
  62. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +0 -0
  63. pygpt_net/provider/llms/base.py +0 -0
  64. pygpt_net/provider/llms/deepseek_api.py +0 -0
  65. pygpt_net/provider/llms/google.py +0 -0
  66. pygpt_net/provider/llms/hugging_face_api.py +0 -0
  67. pygpt_net/provider/llms/hugging_face_router.py +0 -0
  68. pygpt_net/provider/llms/mistral.py +0 -0
  69. pygpt_net/provider/llms/perplexity.py +0 -0
  70. pygpt_net/provider/llms/x_ai.py +0 -0
  71. pygpt_net/tools/agent_builder/tool.py +6 -0
  72. pygpt_net/tools/agent_builder/ui/dialogs.py +0 -41
  73. pygpt_net/ui/layout/toolbox/presets.py +14 -2
  74. pygpt_net/ui/main.py +2 -2
  75. pygpt_net/ui/widget/dialog/confirm.py +55 -5
  76. pygpt_net/ui/widget/draw/painter.py +90 -1
  77. pygpt_net/ui/widget/lists/preset.py +289 -25
  78. pygpt_net/ui/widget/node_editor/editor.py +53 -15
  79. pygpt_net/ui/widget/node_editor/node.py +82 -104
  80. pygpt_net/ui/widget/node_editor/view.py +4 -5
  81. pygpt_net/ui/widget/textarea/input.py +155 -21
  82. {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.63.dist-info}/METADATA +22 -8
  83. {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.63.dist-info}/RECORD +70 -70
  84. {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.63.dist-info}/LICENSE +0 -0
  85. {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.63.dist-info}/WHEEL +0 -0
  86. {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.63.dist-info}/entry_points.txt +0 -0
pygpt_net/CHANGELOG.txt CHANGED
@@ -1,3 +1,15 @@
1
+ 2.6.63 (2025-09-27)
2
+
3
+ - Improved agents' workflows.
4
+ - Enhanced the display of agents' steps in the UI.
5
+
6
+ 2.6.62 (2025-09-26)
7
+
8
+ - Enhanced agent workflow execution.
9
+ - Improved preset list handling by adding a drop field indicator and fixing auto-scroll.
10
+ - Added middle-mouse button panning to Painter.
11
+ - Added an input character counter.
12
+
1
13
  2.6.61 (2025-09-26)
2
14
 
3
15
  - Enhanced the agents node editor, custom agent flow, and instruction following.
pygpt_net/__init__.py CHANGED
@@ -6,15 +6,15 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.09.26 00:00:00 #
9
+ # Updated Date: 2025.09.27 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  __author__ = "Marcin Szczygliński"
13
13
  __copyright__ = "Copyright 2025, Marcin Szczygliński"
14
14
  __credits__ = ["Marcin Szczygliński"]
15
15
  __license__ = "MIT"
16
- __version__ = "2.6.61"
17
- __build__ = "2025-09-26"
16
+ __version__ = "2.6.63"
17
+ __build__ = "2025-09-27"
18
18
  __maintainer__ = "Marcin Szczygliński"
19
19
  __github__ = "https://github.com/szczyglis-dev/py-gpt"
20
20
  __report__ = "https://github.com/szczyglis-dev/py-gpt/issues"
@@ -36,6 +36,7 @@ class Response:
36
36
  """
37
37
  super(Response, self).__init__()
38
38
  self.window = window
39
+ self.last_response_id = None
39
40
 
40
41
  def handle(
41
42
  self,
@@ -273,9 +274,14 @@ class Response:
273
274
  self.window.update_status(trans("status.agent.reasoning"))
274
275
  controller.chat.common.lock_input() # lock input, re-enable stop button
275
276
 
276
- # agent final response
277
+ # agent final response, with fix for async delayed finish (prevent multiple calls for the same response)
277
278
  if ctx.extra is not None and (isinstance(ctx.extra, dict) and "agent_finish" in ctx.extra):
278
- controller.agent.llama.on_finish(ctx) # evaluate response and continue if needed
279
+ consume = False
280
+ if self.last_response_id is None or self.last_response_id < ctx.id:
281
+ consume = True
282
+ self.last_response_id = ctx.id
283
+ if consume:
284
+ controller.agent.llama.on_finish(ctx) # evaluate response and continue if needed
279
285
 
280
286
  def end(
281
287
  self,
@@ -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 00:00:00 #
9
+ # Updated Date: 2025.09.27 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import datetime
@@ -393,6 +393,9 @@ class Editor:
393
393
  value=extra_options[key].get('default', None),
394
394
  )
395
395
 
396
+ # ensure combo defaults are effectively applied for this tab (only empty values are updated)
397
+ self._apply_combo_defaults_for_group(option_key, extra_options)
398
+
396
399
  def load_extra_defaults(self):
397
400
  """Load extra options defaults for preset editor"""
398
401
  if not self.tab_options_idx:
@@ -423,6 +426,8 @@ class Editor:
423
426
  option=extra_options[key],
424
427
  value=value,
425
428
  )
429
+ # ensure combo defaults are effectively applied for this tab (only empty values are updated)
430
+ self._apply_combo_defaults_for_group(option_key, extra_options)
426
431
 
427
432
  def load_extra_defaults_current(self):
428
433
  """Load extra options defaults on mode change"""
@@ -479,6 +484,8 @@ class Editor:
479
484
  option=extra_options[key],
480
485
  value=value,
481
486
  )
487
+ # ensure combo defaults are effectively applied for this tab (only empty values are updated)
488
+ self._apply_combo_defaults_for_group(option_key, extra_options)
482
489
 
483
490
  def append_extra_options(self, preset: PresetItem):
484
491
  """
@@ -785,6 +792,9 @@ class Editor:
785
792
  value=opt_schema.get('default'),
786
793
  )
787
794
 
795
+ # ensure combo defaults are effectively applied for this tab (only empty values are updated)
796
+ self._apply_combo_defaults_for_group(config_id, schema_options)
797
+
788
798
  # 4) Recompute mapping fully based on actual tabs and their 'agent_id' properties.
789
799
  self._rebuild_tab_index_mapping()
790
800
 
@@ -1521,6 +1531,9 @@ class Editor:
1521
1531
  value=opt_schema.get('default'),
1522
1532
  )
1523
1533
 
1534
+ # ensure combo defaults are effectively applied for this tab (only empty values are updated)
1535
+ self._apply_combo_defaults_for_group(config_id, schema_options)
1536
+
1524
1537
  # 7) Recompute the index mapping strictly from the QTabWidget
1525
1538
  self._rebuild_tab_index_mapping()
1526
1539
 
@@ -1529,3 +1542,54 @@ class Editor:
1529
1542
 
1530
1543
  finally:
1531
1544
  tabs.setUpdatesEnabled(True)
1545
+
1546
+ # ---------- Helpers for reliable combo defaults in agent extra options ----------
1547
+
1548
+ def _apply_combo_defaults_for_group(self, parent_id: str, schema_options: Dict[str, Any]) -> None:
1549
+ """
1550
+ Ensure that combo-type inputs inside a given UI config group have their default values applied
1551
+ when the current value is empty ("", None or "_"). This avoids the situation where combo boxes
1552
+ remain uninitialized while other field types receive defaults correctly.
1553
+
1554
+ This function never overrides a non-empty value set by the user or loaded from a preset.
1555
+ """
1556
+ if not schema_options:
1557
+ return
1558
+
1559
+ get_value = self.window.controller.config.get_value
1560
+ apply_value = self.window.controller.config.apply_value
1561
+
1562
+ for key, opt_schema in schema_options.items():
1563
+ if not isinstance(opt_schema, dict):
1564
+ continue
1565
+ if opt_schema.get('type') != 'combo':
1566
+ continue
1567
+
1568
+ default_val = opt_schema.get('default', None)
1569
+ if default_val is None:
1570
+ continue
1571
+
1572
+ current_val = get_value(
1573
+ parent_id=parent_id,
1574
+ key=key,
1575
+ option=opt_schema,
1576
+ )
1577
+
1578
+ # Treat "_", "", None as empty and safe to replace with default
1579
+ if current_val in (None, "", "_"):
1580
+ # First try apply_value (standard path)
1581
+ apply_value(
1582
+ parent_id=parent_id,
1583
+ key=key,
1584
+ option=opt_schema,
1585
+ value=default_val,
1586
+ )
1587
+ # Additionally set directly on widget if accessible to guard against timing of key population
1588
+ try:
1589
+ widget_group = self.window.ui.config.get(parent_id, {})
1590
+ widget = widget_group.get(key)
1591
+ if widget and hasattr(widget, "set_value"):
1592
+ widget.set_value(default_val)
1593
+ except Exception:
1594
+ # Silent fallback; apply_value above should already handle most cases
1595
+ pass
@@ -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.24 23:00:00 #
9
+ # Updated Date: 2025.09.26 13:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -33,6 +33,8 @@ class Profile:
33
33
  self.height = 500
34
34
  self.initialized = False
35
35
  self.dialog_initialized = False
36
+ self.before_theme = None
37
+ self.before_language = None
36
38
 
37
39
  def setup(self):
38
40
  """Setup profile"""
@@ -54,7 +56,8 @@ class Profile:
54
56
  uuid: str,
55
57
  force: bool = False,
56
58
  save_current: bool = True,
57
- on_finish: Optional[callable] = None
59
+ on_finish: Optional[callable] = None,
60
+ is_create: bool = False,
58
61
  ):
59
62
  """
60
63
  Switch profile
@@ -63,6 +66,7 @@ class Profile:
63
66
  :param force: Force switch
64
67
  :param save_current: Save current profile
65
68
  :param on_finish: Callback function to call after switch
69
+ :param is_create: Is called from create profile
66
70
  """
67
71
  current = self.window.core.config.profile.get_current()
68
72
  if uuid == current and not force:
@@ -85,7 +89,8 @@ class Profile:
85
89
  self.window.controller.settings.workdir.update(
86
90
  path,
87
91
  force=True,
88
- profile_name=profile['name']
92
+ profile_name=profile['name'],
93
+ is_create=is_create,
89
94
  )
90
95
  else:
91
96
  self.after_update(profile['name'])
@@ -288,7 +293,14 @@ class Profile:
288
293
 
289
294
  :param uuid: profile UUID
290
295
  """
291
- self.switch(uuid, force=True, on_finish=self.after_create_finish)
296
+ self.before_theme = self.window.core.config.get("theme")
297
+ self.before_language = self.window.core.config.get("lang")
298
+ self.switch(
299
+ uuid,
300
+ force=True,
301
+ on_finish=self.after_create_finish,
302
+ is_create=True
303
+ )
292
304
 
293
305
  def after_create_finish(self, uuid: str):
294
306
  """
@@ -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.20 23:00:00 #
9
+ # Updated Date: 2025.09.26 13:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
@@ -288,13 +288,15 @@ class Workdir:
288
288
  def update_workdir(
289
289
  self,
290
290
  force: bool = False,
291
- path: str = None
291
+ path: str = None,
292
+ is_create: bool = False,
292
293
  ):
293
294
  """
294
295
  Update working directory
295
296
 
296
297
  :param force: boolean indicating if update should be forced (confirm)
297
298
  :param path: new working directory to set
299
+ :param is_create: True if called on profile creation
298
300
  """
299
301
  print("\n====================")
300
302
  print(f"Changing workdir to: {path}")
@@ -313,8 +315,25 @@ class Workdir:
313
315
  # update path in current profile
314
316
  self.window.core.config.profile.update_current_workdir(path)
315
317
 
318
+ # save previous theme and language to retain them after workdir change
319
+ prev_theme = None
320
+ prev_lang = None
321
+ if is_create:
322
+ prev_theme = self.window.core.config.get('theme')
323
+ prev_lang = self.window.core.config.get('lang')
324
+
316
325
  # reload config
317
326
  self.window.core.config.set_workdir(path, reload=True)
327
+
328
+ # if profile is just created, use current theme and language
329
+ if is_create:
330
+ print("Using current theme and language: ", prev_theme, prev_lang)
331
+ if prev_theme is not None:
332
+ self.window.core.config.set('theme', prev_theme)
333
+ if prev_lang is not None:
334
+ self.window.core.config.set('lang', prev_lang)
335
+ self.window.core.config.save()
336
+
318
337
  self.window.core.config.set('license.accepted', True) # accept license to prevent show dialog again
319
338
 
320
339
  @Slot(bool, str, str, str)
@@ -323,7 +342,8 @@ class Workdir:
323
342
  force: bool,
324
343
  profile_name: str,
325
344
  current_path: str,
326
- new_path: str
345
+ new_path: str,
346
+ is_create: bool = False
327
347
  ) -> bool:
328
348
  """
329
349
  Update working directory
@@ -332,18 +352,20 @@ class Workdir:
332
352
  :param profile_name: profile name to update after workdir change
333
353
  :param current_path: current working directory before update
334
354
  :param new_path: new working directory to set
355
+ :param is_create: if True, skip check for existing workdir in path
335
356
  :return: boolean indicating if update was successful
336
357
  """
337
358
  self.update_workdir(
338
359
  force=force,
339
360
  path=new_path,
361
+ is_create=is_create,
340
362
  )
341
363
  rollback = False
342
364
  success = False
343
365
  if force:
344
366
  try:
345
367
  self.window.ui.dialogs.workdir.show_status(trans("dialog.workdir.result.wait"))
346
- self.window.controller.reload()
368
+ self.window.controller.reload() # reload all
347
369
  self.window.ui.dialogs.workdir.show_status(trans("dialog.workdir.result.wait"))
348
370
  msg = trans("dialog.workdir.result.success").format(path=new_path)
349
371
  self.window.ui.dialogs.workdir.show_status(msg)
@@ -498,7 +520,8 @@ class Workdir:
498
520
  self,
499
521
  path: str,
500
522
  force: bool = False,
501
- profile_name: str = None
523
+ profile_name: str = None,
524
+ is_create: bool = False,
502
525
  ):
503
526
  """
504
527
  Switch working directory to the existing one
@@ -506,12 +529,14 @@ class Workdir:
506
529
  :param path: existing working directory
507
530
  :param force: force update (confirm)
508
531
  :param profile_name: profile name (optional, for future use)
532
+ :param is_create: if True, skip check for existing workdir in path
509
533
  """
510
534
  self.do_update(
511
535
  force=force,
512
536
  profile_name=profile_name,
513
537
  current_path=self.window.core.config.get_user_path(),
514
538
  new_path=path,
539
+ is_create=is_create,
515
540
  )
516
541
 
517
542
  def migrate(
@@ -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.07.22 15:00:00 #
9
+ # Updated Date: 2025.09.26 13:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -32,6 +32,8 @@ class Common:
32
32
  :return: custom css filename (e.g. style.dark.css)
33
33
  """
34
34
  # check per theme style css
35
+ if name is None:
36
+ name = ""
35
37
  filename = 'style.css'
36
38
  if filename is not None:
37
39
  # per theme mode (light / dark)
@@ -58,7 +60,7 @@ class Common:
58
60
 
59
61
  :return: True if light theme, False otherwise
60
62
  """
61
- theme = self.window.core.config.get('theme')
63
+ theme = str(self.window.core.config.get('theme'))
62
64
  return theme.startswith('light_') or theme == 'light'
63
65
 
64
66
  def toggle_tooltips(self):
@@ -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
@@ -92,7 +92,7 @@ class Markdown:
92
92
  if base_name == 'web':
93
93
  suffix = "-" + web_style
94
94
  self.web_style = web_style
95
- theme = self.window.core.config.get('theme')
95
+ theme = str(self.window.core.config.get('theme'))
96
96
  name = str(base_name)
97
97
  if theme.startswith('light'):
98
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.09.25 12:00:00 #
9
+ # Updated Date: 2025.09.26 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Optional
@@ -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,7 +153,11 @@ 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()
@@ -161,11 +168,32 @@ class UI:
161
168
  ui_nodes['prompt.context'].setText(ctx_string)
162
169
  self._last_ctx_string = ctx_string
163
170
 
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)}"
171
+ if max_current > 0:
172
+ max_str = short_num(max_current)
173
+ else:
174
+ max_str = "∞"
175
+
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}"
165
177
  if input_string != self._last_input_string:
166
178
  ui_nodes['input.counter'].setText(input_string)
167
179
  self._last_input_string = input_string
168
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
+
169
197
  def store_state(self):
170
198
  """Store UI state"""
171
199
  self.window.controller.layout.scroll_save()
@@ -1,3 +1,5 @@
1
+ # core/agents/runners/llama_workflow.py
2
+
1
3
  #!/usr/bin/env python3
2
4
  # -*- coding: utf-8 -*-
3
5
  # ================================================== #
@@ -6,7 +8,7 @@
6
8
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
9
  # MIT License #
8
10
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.09.25 14:00:00 #
11
+ # Updated Date: 2025.09.27 06:00:00 #
10
12
  # ================================================== #
11
13
 
12
14
  from __future__ import annotations
@@ -207,37 +209,6 @@ class DynamicFlowWorkflowLI(Workflow):
207
209
  return False
208
210
  return False
209
211
 
210
- def _friendly_map(self) -> Dict[str, str]:
211
- return {aid: a.name or aid for aid, a in self.fs.agents.items()}
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
-
241
212
  async def _emit(self, ctx: Context, ev: Any):
242
213
  if self.dbg.event_echo:
243
214
  self.logger.debug(f"[event] emit {ev.__class__.__name__}")
@@ -245,8 +216,8 @@ class DynamicFlowWorkflowLI(Workflow):
245
216
 
246
217
  async def _emit_agent_text(self, ctx: Context, text: str, agent_name: str = "Agent"):
247
218
  """
248
- Emit AgentStream(delta=text) robustly. If your env requires extra fields,
249
- fall back to extended AgentStream like in your SupervisorWorkflow.
219
+ Emit AgentStream(delta=text) robustly. If env requires extra fields,
220
+ fall back to extended AgentStream.
250
221
  """
251
222
  try:
252
223
  if self.dbg.event_echo:
@@ -266,15 +237,25 @@ class DynamicFlowWorkflowLI(Workflow):
266
237
  )
267
238
 
268
239
  async def _emit_header(self, ctx: Context, name: str):
269
- if self.dbg.event_echo:
270
- self.logger.debug(f"[event] header emit begin name='{name}'")
271
- await self._emit_agent_text(ctx, f"\n\n**{name}**\n\n", agent_name=name)
272
- if self.dbg.event_echo:
273
- self.logger.debug("[event] header emit done")
240
+ # Lightweight header to ensure agent name is known before tokens.
241
+ await self._emit_agent_text(ctx, "", agent_name=name)
274
242
 
275
243
  async def _emit_step_sep(self, ctx: Context, node_id: str):
276
244
  try:
277
- await self._emit(ctx, StepEvent(name="next", index=self._steps, total=self.max_iterations, meta={"node": node_id}))
245
+ a = self.fs.agents.get(node_id)
246
+ friendly_name = (a.name if a and a.name else node_id)
247
+ await self._emit(
248
+ ctx,
249
+ StepEvent(
250
+ name="next",
251
+ index=self._steps,
252
+ total=self.max_iterations,
253
+ meta={
254
+ "node": node_id,
255
+ "agent_name": friendly_name, # pass current agent display name
256
+ },
257
+ ),
258
+ )
278
259
  except Exception as e:
279
260
  self.logger.error(f"[event] StepEvent emit failed: {e}")
280
261
 
@@ -335,6 +316,9 @@ class DynamicFlowWorkflowLI(Workflow):
335
316
  return user_msg, [], "no-mem:last_output"
336
317
 
337
318
  async def _update_memory_after_step(self, node_id: str, user_msg_text: str, display_text: str):
319
+ """
320
+ Update per-node memory after a step, storing baton user message and assistant output.
321
+ """
338
322
  mem_id = self.g.agent_to_memory.get(node_id)
339
323
  mem_state = self.mem.get(mem_id) if mem_id else None
340
324
  if not mem_state:
@@ -359,7 +343,7 @@ class DynamicFlowWorkflowLI(Workflow):
359
343
  # ============== Workflow steps ==============
360
344
 
361
345
  def run(self, query: str, ctx: Optional[Context] = None, memory: Any = None, verbose: bool = False, on_stop=None):
362
- """Entry point used by your LlamaWorkflow runner."""
346
+ """Entry point used by LlamaWorkflow runner."""
363
347
  self._on_stop = on_stop
364
348
 
365
349
  # Build initial chat once
@@ -429,8 +413,9 @@ class DynamicFlowWorkflowLI(Workflow):
429
413
  return FlowTickEvent() if self._current_ids else FlowStopEvent(final_answer=self._last_plain_output or "")
430
414
 
431
415
  node: AgentNode = self.fs.agents[current_id]
432
- if self._steps > 1:
433
- await self._emit_step_sep(ctx, current_id)
416
+
417
+ # IMPORTANT: emit StepEvent also for the very first agent step.
418
+ await self._emit_step_sep(ctx, current_id)
434
419
  await self._emit_header(ctx, node.name or current_id)
435
420
 
436
421
  # Resolve runtime + per-node LLM/tools
@@ -459,11 +444,10 @@ class DynamicFlowWorkflowLI(Workflow):
459
444
  f"user='{ellipsize(user_msg_text, self.dbg.preview_chars)}'"
460
445
  )
461
446
 
462
- # Prepare friendly map with optional roles for this node's allowed routes
447
+ # Build agent
463
448
  allowed_routes_now = list(node.outputs or [])
464
- friendly_map = self._friendly_map_for_routes(allowed_routes_now)
449
+ friendly_map = {rid: self.fs.agents.get(rid).name or rid for rid in allowed_routes_now if rid in self.fs.agents}
465
450
 
466
- # Build agent (chat_history/max_iterations in ctor – best practice)
467
451
  built = self.factory.build(
468
452
  node=node,
469
453
  node_runtime=node_rt,
@@ -513,9 +497,6 @@ class DynamicFlowWorkflowLI(Workflow):
513
497
  display_text = decision.content or ""
514
498
  if display_text:
515
499
  await self._emit_agent_text(ctx, display_text, agent_name=(node.name or current_id))
516
- if self.dbg.log_memory_dump:
517
- self.logger.debug(f"[mem.prep] node={current_id} save user='{ellipsize(user_msg_text, self.dbg.preview_chars)}' "
518
- f"assist='{ellipsize(display_text, self.dbg.preview_chars)}'")
519
500
  await self._update_memory_after_step(current_id, user_msg_text, display_text)
520
501
  next_id = decision.route if decision.valid else (allowed_routes[0] if allowed_routes else None)
521
502
  if self.dbg.log_routes:
@@ -526,9 +507,6 @@ class DynamicFlowWorkflowLI(Workflow):
526
507
  display_text = raw_text_clean or ""
527
508
  if display_text:
528
509
  await self._emit_agent_text(ctx, display_text, agent_name=(node.name or current_id))
529
- if self.dbg.log_memory_dump:
530
- self.logger.debug(f"[mem.prep] node={current_id} save user='{ellipsize(user_msg_text, self.dbg.preview_chars)}' "
531
- f"assist='{ellipsize(display_text, self.dbg.preview_chars)}'")
532
510
  await self._update_memory_after_step(current_id, user_msg_text, display_text)
533
511
  outs = self.g.get_next(current_id)
534
512
  next_id = outs[0] if outs else self.g.first_connected_end(current_id)