pygpt-net 2.6.1__py3-none-any.whl → 2.6.6__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 (131) hide show
  1. pygpt_net/CHANGELOG.txt +23 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +20 -1
  4. pygpt_net/config.py +55 -65
  5. pygpt_net/controller/__init__.py +5 -2
  6. pygpt_net/controller/calendar/note.py +101 -126
  7. pygpt_net/controller/chat/chat.py +38 -35
  8. pygpt_net/controller/chat/render.py +154 -214
  9. pygpt_net/controller/chat/response.py +5 -3
  10. pygpt_net/controller/chat/stream.py +92 -27
  11. pygpt_net/controller/config/config.py +39 -42
  12. pygpt_net/controller/config/field/checkbox.py +16 -12
  13. pygpt_net/controller/config/field/checkbox_list.py +36 -31
  14. pygpt_net/controller/config/field/cmd.py +51 -57
  15. pygpt_net/controller/config/field/combo.py +33 -16
  16. pygpt_net/controller/config/field/dictionary.py +48 -55
  17. pygpt_net/controller/config/field/input.py +50 -32
  18. pygpt_net/controller/config/field/slider.py +40 -45
  19. pygpt_net/controller/config/field/textarea.py +20 -6
  20. pygpt_net/controller/config/placeholder.py +110 -231
  21. pygpt_net/controller/ctx/common.py +48 -48
  22. pygpt_net/controller/ctx/ctx.py +91 -132
  23. pygpt_net/controller/lang/mapping.py +57 -95
  24. pygpt_net/controller/lang/plugins.py +64 -55
  25. pygpt_net/controller/lang/settings.py +39 -38
  26. pygpt_net/controller/layout/layout.py +176 -109
  27. pygpt_net/controller/mode/mode.py +88 -85
  28. pygpt_net/controller/model/model.py +73 -73
  29. pygpt_net/controller/plugins/plugins.py +209 -223
  30. pygpt_net/controller/plugins/presets.py +54 -55
  31. pygpt_net/controller/plugins/settings.py +54 -69
  32. pygpt_net/controller/presets/editor.py +33 -88
  33. pygpt_net/controller/presets/experts.py +20 -1
  34. pygpt_net/controller/presets/presets.py +293 -298
  35. pygpt_net/controller/settings/profile.py +16 -4
  36. pygpt_net/controller/theme/theme.py +72 -81
  37. pygpt_net/controller/ui/mode.py +118 -186
  38. pygpt_net/controller/ui/tabs.py +69 -90
  39. pygpt_net/controller/ui/ui.py +47 -56
  40. pygpt_net/controller/ui/vision.py +24 -23
  41. pygpt_net/core/agents/runner.py +15 -7
  42. pygpt_net/core/bridge/bridge.py +5 -5
  43. pygpt_net/core/command/command.py +149 -219
  44. pygpt_net/core/ctx/ctx.py +94 -146
  45. pygpt_net/core/debug/debug.py +48 -58
  46. pygpt_net/core/experts/experts.py +3 -3
  47. pygpt_net/core/models/models.py +74 -112
  48. pygpt_net/core/modes/modes.py +13 -21
  49. pygpt_net/core/plugins/plugins.py +154 -177
  50. pygpt_net/core/presets/presets.py +103 -176
  51. pygpt_net/core/render/web/body.py +217 -215
  52. pygpt_net/core/render/web/renderer.py +330 -474
  53. pygpt_net/core/text/utils.py +28 -44
  54. pygpt_net/core/tokens/tokens.py +104 -203
  55. pygpt_net/data/config/config.json +3 -3
  56. pygpt_net/data/config/models.json +3 -3
  57. pygpt_net/data/locale/locale.de.ini +2 -0
  58. pygpt_net/data/locale/locale.en.ini +2 -0
  59. pygpt_net/data/locale/locale.es.ini +2 -0
  60. pygpt_net/data/locale/locale.fr.ini +2 -0
  61. pygpt_net/data/locale/locale.it.ini +2 -0
  62. pygpt_net/data/locale/locale.pl.ini +3 -1
  63. pygpt_net/data/locale/locale.uk.ini +2 -0
  64. pygpt_net/data/locale/locale.zh.ini +2 -0
  65. pygpt_net/item/ctx.py +141 -139
  66. pygpt_net/plugin/agent/plugin.py +2 -1
  67. pygpt_net/plugin/audio_output/plugin.py +5 -2
  68. pygpt_net/plugin/base/plugin.py +101 -85
  69. pygpt_net/plugin/bitbucket/__init__.py +12 -0
  70. pygpt_net/plugin/bitbucket/config.py +267 -0
  71. pygpt_net/plugin/bitbucket/plugin.py +126 -0
  72. pygpt_net/plugin/bitbucket/worker.py +569 -0
  73. pygpt_net/plugin/cmd_code_interpreter/plugin.py +3 -2
  74. pygpt_net/plugin/cmd_custom/plugin.py +3 -2
  75. pygpt_net/plugin/cmd_files/plugin.py +3 -2
  76. pygpt_net/plugin/cmd_history/plugin.py +3 -2
  77. pygpt_net/plugin/cmd_mouse_control/plugin.py +5 -2
  78. pygpt_net/plugin/cmd_serial/plugin.py +3 -2
  79. pygpt_net/plugin/cmd_system/plugin.py +3 -6
  80. pygpt_net/plugin/cmd_web/plugin.py +3 -2
  81. pygpt_net/plugin/experts/plugin.py +2 -2
  82. pygpt_net/plugin/facebook/__init__.py +12 -0
  83. pygpt_net/plugin/facebook/config.py +359 -0
  84. pygpt_net/plugin/facebook/plugin.py +113 -0
  85. pygpt_net/plugin/facebook/worker.py +698 -0
  86. pygpt_net/plugin/github/__init__.py +12 -0
  87. pygpt_net/plugin/github/config.py +441 -0
  88. pygpt_net/plugin/github/plugin.py +126 -0
  89. pygpt_net/plugin/github/worker.py +674 -0
  90. pygpt_net/plugin/google/__init__.py +12 -0
  91. pygpt_net/plugin/google/config.py +367 -0
  92. pygpt_net/plugin/google/plugin.py +126 -0
  93. pygpt_net/plugin/google/worker.py +826 -0
  94. pygpt_net/plugin/idx_llama_index/plugin.py +3 -2
  95. pygpt_net/plugin/mailer/plugin.py +3 -5
  96. pygpt_net/plugin/openai_vision/plugin.py +3 -2
  97. pygpt_net/plugin/real_time/plugin.py +52 -60
  98. pygpt_net/plugin/slack/__init__.py +12 -0
  99. pygpt_net/plugin/slack/config.py +349 -0
  100. pygpt_net/plugin/slack/plugin.py +115 -0
  101. pygpt_net/plugin/slack/worker.py +639 -0
  102. pygpt_net/plugin/telegram/__init__.py +12 -0
  103. pygpt_net/plugin/telegram/config.py +308 -0
  104. pygpt_net/plugin/telegram/plugin.py +117 -0
  105. pygpt_net/plugin/telegram/worker.py +563 -0
  106. pygpt_net/plugin/twitter/__init__.py +12 -0
  107. pygpt_net/plugin/twitter/config.py +491 -0
  108. pygpt_net/plugin/twitter/plugin.py +125 -0
  109. pygpt_net/plugin/twitter/worker.py +837 -0
  110. pygpt_net/provider/agents/llama_index/legacy/openai_assistant.py +35 -3
  111. pygpt_net/tools/code_interpreter/tool.py +0 -1
  112. pygpt_net/tools/translator/tool.py +1 -1
  113. pygpt_net/ui/base/config_dialog.py +86 -100
  114. pygpt_net/ui/base/context_menu.py +48 -46
  115. pygpt_net/ui/dialog/preset.py +34 -77
  116. pygpt_net/ui/layout/ctx/ctx_list.py +10 -6
  117. pygpt_net/ui/layout/toolbox/presets.py +41 -41
  118. pygpt_net/ui/main.py +49 -31
  119. pygpt_net/ui/tray.py +61 -60
  120. pygpt_net/ui/widget/calendar/select.py +86 -70
  121. pygpt_net/ui/widget/lists/attachment.py +86 -44
  122. pygpt_net/ui/widget/lists/base_list_combo.py +85 -33
  123. pygpt_net/ui/widget/lists/context.py +135 -188
  124. pygpt_net/ui/widget/lists/preset.py +59 -61
  125. pygpt_net/ui/widget/textarea/web.py +161 -48
  126. pygpt_net/utils.py +8 -1
  127. {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.6.dist-info}/METADATA +164 -2
  128. {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.6.dist-info}/RECORD +131 -103
  129. {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.6.dist-info}/LICENSE +0 -0
  130. {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.6.dist-info}/WHEEL +0 -0
  131. {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.6.dist-info}/entry_points.txt +0 -0
@@ -6,17 +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.08.14 01:00:00 #
9
+ # Updated Date: 2025.08.16 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
- import html
13
12
  import json
14
13
  import os
15
14
  import re
16
15
  from datetime import datetime
17
- from typing import Optional, List, Dict
18
-
19
- from PySide6.QtCore import QTimer
16
+ from typing import Optional, List
20
17
 
21
18
  from pygpt_net.core.render.base import BaseRenderer
22
19
  from pygpt_net.core.text.utils import has_unclosed_code_tag
@@ -36,6 +33,16 @@ from pygpt_net.core.events import RenderEvent
36
33
  class Renderer(BaseRenderer):
37
34
  NODE_INPUT = 0
38
35
  NODE_OUTPUT = 1
36
+ ENDINGS_CODE = (
37
+ "</code></pre></div>",
38
+ "</code></pre></div><br/>",
39
+ "</code></pre></div><br>"
40
+ )
41
+ ENDINGS_LIST = (
42
+ "</ul>",
43
+ "</ol>",
44
+ "</li>"
45
+ )
39
46
 
40
47
  def __init__(self, window=None):
41
48
  super(Renderer, self).__init__(window)
@@ -51,16 +58,17 @@ class Renderer(BaseRenderer):
51
58
  self.pids = {} # per node data
52
59
  self.prev_chunk_replace = False
53
60
  self.prev_chunk_newline = False
54
- self._stream_flush_delay_ms = 12
55
- self._stream_max_pending_bytes = 8192
56
- self._stream_immediate_on_newline = True
57
- self._stream_state: Dict[int, dict] = {}
61
+
62
+ app_path = self.window.core.config.get_app_path() if self.window else ""
63
+ self._icon_expand = os.path.join(app_path, "data", "icons", "expand.svg")
64
+ self._icon_sync = os.path.join(app_path, "data", "icons", "sync.svg")
65
+ self._file_prefix = 'file:///' if self.window and self.window.core.platforms.is_windows() else 'file://'
58
66
 
59
67
  def prepare(self):
60
68
  """
61
69
  Prepare renderer
62
70
  """
63
- self.pids.clear()
71
+ self.pids = {}
64
72
 
65
73
  def on_load(self, meta: CtxMeta = None):
66
74
  """
@@ -73,7 +81,7 @@ class Renderer(BaseRenderer):
73
81
  self.reset(meta)
74
82
  self.parser.reset()
75
83
  try:
76
- node.page().runJavaScript(f"if (typeof window.prepare !== 'undefined') prepare();")
84
+ node.page().runJavaScript("if (typeof window.prepare !== 'undefined') prepare();")
77
85
  except Exception as e:
78
86
  pass
79
87
 
@@ -88,28 +96,27 @@ class Renderer(BaseRenderer):
88
96
  :param meta: context meta
89
97
  :param tab: Tab
90
98
  """
91
- if meta is None:
99
+ if meta is None or tab is None:
92
100
  return
93
101
  pid = tab.pid
94
102
  if pid is None or pid not in self.pids:
95
103
  return
96
- p = self.pids[pid]
97
- p.loaded = True
104
+ self.pids[pid].loaded = True
98
105
  node = self.get_output_node(meta)
99
106
 
100
- if p.html != "" and not p.use_buffer:
107
+ if self.pids[pid].html != "" and not self.pids[pid].use_buffer:
101
108
  self.clear_chunks_input(pid)
102
109
  self.clear_chunks_output(pid)
103
110
  self.clear_nodes(pid)
104
- self.append(pid, p.html, flush=True)
105
- p.html = ""
111
+ self.append(pid, self.pids[pid].html, flush=True)
112
+ self.pids[pid].html = ""
106
113
 
107
114
  node.setUpdatesEnabled(True)
108
115
 
109
116
  def get_pid(self, meta: CtxMeta):
110
117
  """
111
118
  Get PID for context meta
112
-
119
+
113
120
  :param meta: context PID
114
121
  """
115
122
  return self.window.core.ctx.output.get_pid(meta)
@@ -117,7 +124,7 @@ class Renderer(BaseRenderer):
117
124
  def get_or_create_pid(self, meta: CtxMeta):
118
125
  """
119
126
  Get PID for context meta and create PID data (if not exists)
120
-
127
+
121
128
  :param meta: context PID
122
129
  """
123
130
  if meta is not None:
@@ -133,7 +140,7 @@ class Renderer(BaseRenderer):
133
140
  ):
134
141
  """
135
142
  Create PID data
136
-
143
+
137
144
  :param pid: PID
138
145
  :param meta: context meta
139
146
  """
@@ -163,7 +170,6 @@ class Renderer(BaseRenderer):
163
170
  :param state: state name
164
171
  :param meta: context meta
165
172
  """
166
- # BUSY: current pid only
167
173
  if state == RenderEvent.STATE_BUSY:
168
174
  if meta:
169
175
  pid = self.get_pid(meta)
@@ -171,29 +177,27 @@ class Renderer(BaseRenderer):
171
177
  node = self.get_output_node_by_pid(pid)
172
178
  try:
173
179
  node.page().runJavaScript(
174
- f"if (typeof window.showLoading !== 'undefined') showLoading();")
180
+ "if (typeof window.showLoading !== 'undefined') showLoading();")
175
181
  except Exception as e:
176
182
  pass
177
183
 
178
- # IDLE: all pids
179
184
  elif state == RenderEvent.STATE_IDLE:
180
- for pid in self.pids.keys():
185
+ for pid in self.pids:
181
186
  node = self.get_output_node_by_pid(pid)
182
187
  if node is not None:
183
188
  try:
184
189
  node.page().runJavaScript(
185
- f"if (typeof window.hideLoading !== 'undefined') hideLoading();")
190
+ "if (typeof window.hideLoading !== 'undefined') hideLoading();")
186
191
  except Exception as e:
187
192
  pass
188
193
 
189
- # ERROR: all pids
190
194
  elif state == RenderEvent.STATE_ERROR:
191
- for pid in self.pids.keys():
195
+ for pid in self.pids:
192
196
  node = self.get_output_node_by_pid(pid)
193
197
  if node is not None:
194
198
  try:
195
199
  node.page().runJavaScript(
196
- f"if (typeof window.hideLoading !== 'undefined') hideLoading();")
200
+ "if (typeof window.hideLoading !== 'undefined') hideLoading();")
197
201
  except Exception as e:
198
202
  pass
199
203
 
@@ -213,7 +217,7 @@ class Renderer(BaseRenderer):
213
217
  pid = self.get_or_create_pid(meta)
214
218
  self.init(pid)
215
219
  self.reset_names(meta)
216
- self.tool_output_end() # reset tools
220
+ self.tool_output_end()
217
221
  self.prev_chunk_replace = False
218
222
 
219
223
  def end(
@@ -229,14 +233,13 @@ class Renderer(BaseRenderer):
229
233
  :param ctx: context item
230
234
  :param stream: True if it is a stream
231
235
  """
232
- self._stream_state.clear() # reset stream state
233
236
  pid = self.get_or_create_pid(meta)
234
237
  if pid is None:
235
238
  return
236
- p = self.pids[pid]
237
- if p.item is not None and stream:
238
- self.append_context_item(meta, p.item)
239
- p.item = None
239
+ if self.pids[pid].item is not None and stream:
240
+ self.append_context_item(meta, self.pids[pid].item)
241
+ self.pids[pid].item = None
242
+ self.pids[pid].clear()
240
243
 
241
244
  def end_extra(
242
245
  self,
@@ -246,7 +249,7 @@ class Renderer(BaseRenderer):
246
249
  ):
247
250
  """
248
251
  Render end extra
249
-
252
+
250
253
  :param meta: context meta
251
254
  :param ctx: context item
252
255
  :param stream: True if it is a stream
@@ -260,10 +263,10 @@ class Renderer(BaseRenderer):
260
263
  ):
261
264
  """
262
265
  Render stream begin
263
-
266
+
264
267
  :param meta: context meta
265
268
  :param ctx: context item
266
- """
269
+ """
267
270
  self.prev_chunk_replace = False
268
271
  try:
269
272
  self.get_output_node(meta).page().runJavaScript("beginStream();")
@@ -277,23 +280,19 @@ class Renderer(BaseRenderer):
277
280
  ):
278
281
  """
279
282
  Render stream end
280
-
283
+
281
284
  :param meta: context meta
282
285
  :param ctx: context item
283
286
  """
284
- self._stream_state.clear() # reset stream state
285
287
  self.prev_chunk_replace = False
286
288
  pid = self.get_or_create_pid(meta)
287
289
  if pid is None:
288
290
  return
289
-
290
- p = self.pids[pid]
291
291
  if self.window.controller.agent.legacy.enabled():
292
- if p.item is not None:
293
- self.append_context_item(meta, p.item)
294
- p.item = None
295
-
296
- p.clear() # reset buffers
292
+ if self.pids[pid].item is not None:
293
+ self.append_context_item(meta, self.pids[pid].item)
294
+ self.pids[pid].item = None
295
+ self.pids[pid].clear()
297
296
  try:
298
297
  self.get_output_node(meta).page().runJavaScript("endStream();")
299
298
  except Exception as e:
@@ -307,7 +306,7 @@ class Renderer(BaseRenderer):
307
306
  ):
308
307
  """
309
308
  Append all context to output
310
-
309
+
311
310
  :param meta: Context meta
312
311
  :param items: context items
313
312
  :param clear: True if clear all output before append
@@ -321,35 +320,32 @@ class Renderer(BaseRenderer):
321
320
 
322
321
  if clear:
323
322
  self.reset(meta)
324
- i = 0
325
323
 
326
- p = self.pids[pid]
327
- p.use_buffer = True
328
- p.html = ""
324
+ self.pids[pid].use_buffer = True
325
+ self.pids[pid].html = ""
329
326
  prev_ctx = None
330
- for item in items:
327
+ total = len(items)
328
+ for i, item in enumerate(items):
331
329
  self.update_names(meta, item)
332
330
  item.idx = i
333
331
  if i == 0:
334
332
  item.first = True
335
- next_item = items[i + 1] if i + 1 < len(items) else None # append next item if exists
333
+ next_item = items[i + 1] if i + 1 < total else None
336
334
  self.append_context_item(
337
335
  meta,
338
336
  item,
339
337
  prev_ctx=prev_ctx,
340
338
  next_ctx=next_item
341
- ) # to html buffer
339
+ )
342
340
  prev_ctx = item
343
- i += 1
344
- p.use_buffer = False
341
+ self.pids[pid].use_buffer = False
345
342
 
346
- # flush
347
- if p.html != "":
343
+ if self.pids[pid].html != "":
348
344
  self.append(
349
345
  pid,
350
- p.html,
346
+ self.pids[pid].html,
351
347
  flush=True
352
- ) # flush buffer if page loaded, otherwise it will be flushed on page load
348
+ )
353
349
 
354
350
  def append_input(
355
351
  self, meta: CtxMeta,
@@ -365,7 +361,7 @@ class Renderer(BaseRenderer):
365
361
  :param flush: flush HTML
366
362
  :param append: True if force append node
367
363
  """
368
- self.tool_output_end() # reset tools
364
+ self.tool_output_end()
369
365
  pid = self.get_or_create_pid(meta)
370
366
  if not flush:
371
367
  self.clear_chunks_input(pid)
@@ -376,7 +372,6 @@ class Renderer(BaseRenderer):
376
372
 
377
373
  text = ctx.input
378
374
 
379
- # if sub-reply
380
375
  if isinstance(ctx.extra, dict) and "sub_reply" in ctx.extra and ctx.extra["sub_reply"]:
381
376
  try:
382
377
  json_encoded = json.loads(text)
@@ -387,18 +382,16 @@ class Renderer(BaseRenderer):
387
382
  except json.JSONDecodeError:
388
383
  pass
389
384
 
390
- # hidden internal call
391
385
  if ctx.internal \
392
386
  and not ctx.first \
393
387
  and not ctx.input.strip().startswith("user: ") \
394
- and not ctx.input.strip().startswith("@"): # expert says:
388
+ and not ctx.input.strip().startswith("@"):
395
389
  return
396
390
  else:
397
- # don't show user prefix if provided in internal call goal update
398
391
  if ctx.internal and ctx.input.startswith("user: "):
399
392
  text = re.sub(r'^user: ', '> ', ctx.input)
400
393
 
401
- if flush: # to chunk buffer
394
+ if flush:
402
395
  if self.is_stream() and not append:
403
396
  content = self.prepare_node(meta, ctx, text.strip(), self.NODE_INPUT)
404
397
  self.append_chunk_input(meta, ctx, content, False)
@@ -416,24 +409,22 @@ class Renderer(BaseRenderer):
416
409
  ):
417
410
  """
418
411
  Append text output to output
419
-
412
+
420
413
  :param meta: context meta
421
414
  :param ctx: context item
422
415
  :param flush: flush HTML
423
416
  :param prev_ctx: previous context
424
417
  :param next_ctx: next context
425
418
  """
426
- self.tool_output_end() # reset tools
419
+ self.tool_output_end()
427
420
  output = ctx.output
428
- if (isinstance(ctx.extra, dict)
429
- and "output" in ctx.extra
430
- and ctx.extra["output"]):
421
+ if isinstance(ctx.extra, dict) and ctx.extra.get("output"):
431
422
  if self.window.core.config.get("llama.idx.chat.agent.render.all", False):
432
- output = "__agent_begin__" + ctx.output + "__agent_end__" + ctx.extra["output"]
423
+ output = f"__agent_begin__{ctx.output}__agent_end__{ctx.extra['output']}"
433
424
  else:
434
425
  output = ctx.extra["output"]
435
426
  else:
436
- if ctx.output is None or ctx.output == "":
427
+ if not output:
437
428
  return
438
429
  self.append_node(
439
430
  meta=meta,
@@ -444,124 +435,6 @@ class Renderer(BaseRenderer):
444
435
  next_ctx=next_ctx
445
436
  )
446
437
 
447
- def _get_stream_state(self, pid: int):
448
- """
449
- Get stream state for PID
450
-
451
- :param pid: context PID
452
- :return: dict – stream state
453
- """
454
- # pid state for micro chunk
455
- st = self._stream_state.get(pid)
456
- if st is None:
457
- st = {
458
- "pending": [], # list[str] – uncommitted chunks
459
- "pending_bytes": 0, # fast check for flush
460
- "scheduled": False, # is flush scheduled?
461
- "gen": 0, # count of stream generations (for flush)
462
- "last_ctx": None, # last ctx for name_header
463
- "last_meta": None, # last meta (to get_output_node)
464
- }
465
- self._stream_state[pid] = st
466
- return st
467
-
468
- def _schedule_flush(
469
- self,
470
- pid: int,
471
- gen: int,
472
- delay_ms: int
473
- ):
474
- """
475
- Schedule flush for stream output
476
-
477
- :param pid: context PID
478
- :param gen: generation number (to avoid flush on reset)
479
- :param delay_ms: delay in milliseconds
480
- """
481
- # no leaks, only primitive closures
482
- def _run():
483
- # if reset or new generation, don't flush
484
- st = self._stream_state.get(pid)
485
- if not st or st["gen"] != gen:
486
- return
487
- self._flush_stream(pid)
488
- QTimer.singleShot(max(0, int(delay_ms)), _run)
489
-
490
- def _flush_stream(self, pid: int):
491
- """
492
- Flush stream output
493
-
494
- :param pid: context PID
495
- """
496
- st = self._get_stream_state(pid)
497
- if not st["pending"]:
498
- st["scheduled"] = False
499
- return
500
-
501
- p = self.pids[pid]
502
-
503
- # confirm cumulated chunks
504
- raw_chunk = "".join(st["pending"])
505
- st["pending"].clear()
506
- st["pending_bytes"] = 0
507
- st["scheduled"] = False
508
-
509
- # header+update_names based on last ctx
510
- ctx = st["last_ctx"] if st["last_ctx"] is not None else p.item
511
- meta = st["last_meta"]
512
-
513
- name_header = self.get_name_header(ctx)
514
- self.update_names(meta, ctx)
515
-
516
- # buffer commited only on flush (less copies)
517
- # begin reset was already called in append_chunk gdy begin=True
518
- p.append_buffer(raw_chunk)
519
- buffer = p.buffer
520
-
521
- has_unclosed = has_unclosed_code_tag
522
- parse_buffer = buffer + "\n```" if has_unclosed(buffer) else buffer
523
- html = self.parser.parse(parse_buffer)
524
-
525
- code_endings = ("</code></pre></div>", "</code></pre></div><br/>", "</code></pre></div><br>")
526
- list_endings = ("</ul>", "</ol>", "</li>")
527
- is_code_block = html.endswith(code_endings)
528
- is_list_block = html.endswith(list_endings)
529
-
530
- newline_in_chunk = "\n" in raw_chunk
531
- is_newline = newline_in_chunk or buffer.endswith("\n") or is_code_block or is_list_block
532
-
533
- force_replace = self.prev_chunk_newline
534
- self.prev_chunk_newline = newline_in_chunk
535
-
536
- replace_bool = is_newline or force_replace
537
- if is_code_block and not newline_in_chunk:
538
- replace_bool = False
539
-
540
- # prepare for DOM
541
- out_chunk = raw_chunk
542
- if not is_code_block:
543
- out_chunk = out_chunk.replace("\n", "<br/>")
544
- else:
545
- if self.prev_chunk_replace and not has_unclosed(out_chunk):
546
- out_chunk = "\n" + out_chunk
547
-
548
- # JSON-escape
549
- name_header_json = json.dumps(name_header)
550
- html_json = json.dumps(html)
551
- chunk_json = json.dumps(out_chunk)
552
-
553
- self.prev_chunk_replace = replace_bool
554
-
555
- replace_js = "true" if replace_bool else "false"
556
- code_block_js = "true" if is_code_block else "false"
557
-
558
- try:
559
- self.get_output_node(meta).page().runJavaScript(
560
- f"appendStream({name_header_json}, {html_json}, {chunk_json}, {replace_js}, {code_block_js});"
561
- )
562
- except Exception:
563
- pass
564
-
565
438
  def append_chunk(
566
439
  self,
567
440
  meta: CtxMeta,
@@ -570,7 +443,7 @@ class Renderer(BaseRenderer):
570
443
  begin: bool = False
571
444
  ):
572
445
  """
573
- Append output chunk to output (micro-batching + delay)
446
+ Append output chunk to output
574
447
 
575
448
  :param meta: context meta
576
449
  :param ctx: context item
@@ -578,56 +451,74 @@ class Renderer(BaseRenderer):
578
451
  :param begin: if it is the beginning of the text
579
452
  """
580
453
  pid = self.get_or_create_pid(meta)
581
- p = self.pids[pid]
582
- p.item = ctx
583
-
584
- # empty chunk
454
+ pctx = self.pids[pid]
455
+ pctx.item = ctx
585
456
  if not text_chunk:
586
457
  if begin:
587
- p.buffer = "" # always reset buffer
458
+ pctx.clear()
588
459
  return
589
460
 
590
- st = self._get_stream_state(pid)
591
- st["last_ctx"] = ctx
592
- st["last_meta"] = meta
461
+ name_header_str = self.get_name_header(ctx)
462
+ self.update_names(meta, ctx)
463
+ text_chunk = text_chunk if isinstance(text_chunk, str) else str(text_chunk)
464
+ text_chunk = text_chunk.translate({ord('<'): '&lt;', ord('>'): '&gt;'})
593
465
 
594
- # on begin, reset generation and pending chunks
595
- raw_chunk = text_chunk if isinstance(text_chunk, str) else str(text_chunk)
596
466
  if begin:
597
- debug = self.append_debug(ctx, pid, "stream") if self.is_debug() else ""
598
- if debug:
599
- raw_chunk = debug + raw_chunk
600
- p.buffer = "" # reset buffer
601
- p.is_cmd = False # reset command flag
467
+ if self.is_debug():
468
+ debug = self.append_debug(ctx, pid, "stream")
469
+ if debug:
470
+ text_chunk = debug + text_chunk
471
+ pctx.clear() # reset buffer
472
+ pctx.is_cmd = False # reset command flag
602
473
  self.clear_chunks_output(pid)
603
474
  self.prev_chunk_replace = False
604
475
 
605
- # micro-batch reset
606
- st["pending"].clear()
607
- st["pending_bytes"] = 0
608
- st["gen"] += 1
609
- st["scheduled"] = False
610
-
611
- # accumulation in list (no fragmentation by +=)
612
- st["pending"].append(raw_chunk)
613
- st["pending_bytes"] += len(raw_chunk)
614
-
615
- # Flush:
616
- # - immediate on newline/``` or huge amount of pending bytes
617
- # - otherwise delay (12 ms)
618
- immediate = False
619
- if self._stream_immediate_on_newline and ("\n" in raw_chunk or "```" in raw_chunk):
620
- immediate = True
621
- if st["pending_bytes"] >= self._stream_max_pending_bytes:
622
- immediate = True
623
-
624
- if not st["scheduled"]:
625
- st["scheduled"] = True
626
- delay = 0 if immediate else self._stream_flush_delay_ms
627
- gen_snapshot = st["gen"]
628
- self._schedule_flush(pid, gen_snapshot, delay)
476
+ pctx.append_buffer(text_chunk)
477
+
478
+ buffer = pctx.buffer
479
+ if has_unclosed_code_tag(buffer):
480
+ buffer_to_parse = "".join((buffer, "\n```"))
481
+ else:
482
+ buffer_to_parse = buffer
483
+
484
+ html = self.parser.parse(buffer_to_parse)
485
+ is_code_block = html.endswith(self.ENDINGS_CODE)
486
+ is_list = html.endswith(self.ENDINGS_LIST)
487
+ is_newline = ("\n" in text_chunk) or buffer.endswith("\n") or is_code_block
488
+ force_replace = False
489
+ if self.prev_chunk_newline:
490
+ force_replace = True
491
+ if "\n" in text_chunk:
492
+ self.prev_chunk_newline = True
493
+ else:
494
+ self.prev_chunk_newline = False
495
+
496
+ replace_bool = False
497
+ if is_newline or force_replace or is_list:
498
+ replace_bool = True
499
+ if is_code_block:
500
+ # don't replace if it is a code block
501
+ if "\n" not in text_chunk:
502
+ # if there is no newline in raw_chunk, then don't replace
503
+ replace_bool = False
504
+
505
+ if not is_code_block:
506
+ text_chunk = text_chunk.replace("\n", "<br/>")
629
507
  else:
630
- # already planned, flush will be executed later
508
+ if self.prev_chunk_replace and not has_unclosed_code_tag(text_chunk):
509
+ # if previous chunk was replaced and current is code block, then add \n to chunk
510
+ text_chunk = "".join(("\n", text_chunk)) # add newline to chunk
511
+
512
+ self.prev_chunk_replace = replace_bool
513
+ try:
514
+ self.get_output_node(meta).page().bridge.chunk.emit(
515
+ name_header_str or "",
516
+ html if replace_bool else "",
517
+ text_chunk if not replace_bool else "",
518
+ bool(replace_bool),
519
+ bool(is_code_block),
520
+ )
521
+ except Exception as e:
631
522
  pass
632
523
 
633
524
  def next_chunk(
@@ -642,15 +533,14 @@ class Renderer(BaseRenderer):
642
533
  :param ctx: context item
643
534
  """
644
535
  pid = self.get_or_create_pid(meta)
645
- p = self.pids[pid]
646
- p.item = ctx
647
- p.buffer = "" # always reset buffer
536
+ self.pids[pid].item = ctx
537
+ self.pids[pid].buffer = ""
648
538
  self.update_names(meta, ctx)
649
539
  self.prev_chunk_replace = False
650
540
  self.prev_chunk_newline = False
651
541
  try:
652
542
  self.get_output_node(meta).page().runJavaScript(
653
- f"nextStream();")
543
+ "nextStream();")
654
544
  except Exception as e:
655
545
  pass
656
546
 
@@ -699,32 +589,27 @@ class Renderer(BaseRenderer):
699
589
  :param begin: if it is the beginning of the text
700
590
  """
701
591
  pid = self.get_or_create_pid(meta)
702
- p = self.pids[pid]
703
- p.item = ctx
592
+ self.pids[pid].item = ctx
704
593
  if text_chunk is None or text_chunk == "":
705
594
  if begin:
706
- p.live_buffer = "" # always reset buffer
595
+ self.pids[pid].live_buffer = ""
707
596
  return
708
597
  self.update_names(meta, ctx)
709
- raw_chunk = str(text_chunk)
710
- raw_chunk = raw_chunk.replace("<", "&lt;")
711
- raw_chunk = raw_chunk.replace(">", "&gt;")
598
+ raw_chunk = str(text_chunk).translate({ord('<'): '&lt;', ord('>'): '&gt;'})
712
599
  if begin:
713
- # debug
714
600
  debug = ""
715
601
  if self.is_debug():
716
602
  debug = self.append_debug(ctx, pid, "stream")
717
603
  if debug:
718
604
  raw_chunk = debug + raw_chunk
719
- p.live_buffer = "" # reset buffer
720
- p.is_cmd = False # reset command flag
721
- self.clear_live(meta, ctx) # clear live output
722
- p.append_live_buffer(raw_chunk)
723
-
724
- # parse chunks
725
- to_append = p.live_buffer
726
- if has_unclosed_code_tag(p.live_buffer):
727
- to_append += "\n```" # fix for code block without closing ```
605
+ self.pids[pid].live_buffer = ""
606
+ self.pids[pid].is_cmd = False
607
+ self.clear_live(meta, ctx)
608
+ self.pids[pid].append_live_buffer(raw_chunk)
609
+
610
+ to_append = self.pids[pid].live_buffer
611
+ if has_unclosed_code_tag(self.pids[pid].live_buffer):
612
+ to_append += "\n```"
728
613
  html = self.parser.parse(to_append)
729
614
  escaped_chunk = json.dumps(html)
730
615
  try:
@@ -744,7 +629,7 @@ class Renderer(BaseRenderer):
744
629
  return
745
630
  pid = self.get_or_create_pid(meta)
746
631
  if not self.pids[pid].loaded:
747
- js = "var element = document.getElementById('_append_live_'); if (element) { element.innerHTML = ''; }"
632
+ js = "var element = document.getElementById('_append_live_');if (element) { element.innerHTML = ''; }"
748
633
  else:
749
634
  js = "clearLive();"
750
635
  try:
@@ -795,19 +680,18 @@ class Renderer(BaseRenderer):
795
680
  ):
796
681
  """
797
682
  Append text to output
798
-
683
+
799
684
  :param pid: ctx pid
800
685
  :param html: HTML code
801
686
  :param flush: True if flush only
802
687
  """
803
- p = self.pids[pid]
804
- if p.loaded and not p.use_buffer:
688
+ if self.pids[pid].loaded and not self.pids[pid].use_buffer:
805
689
  self.clear_chunks(pid)
806
- self.flush_output(pid, html) # render
807
- p.html = ""
690
+ self.flush_output(pid, html)
691
+ self.pids[pid].html = ""
808
692
  else:
809
693
  if not flush:
810
- p.append_html(html) # to buffer
694
+ self.pids[pid].append_html(html)
811
695
 
812
696
  def append_context_item(
813
697
  self,
@@ -835,7 +719,7 @@ class Renderer(BaseRenderer):
835
719
  flush=False,
836
720
  prev_ctx=prev_ctx,
837
721
  next_ctx=next_ctx
838
- ) # + extra
722
+ )
839
723
 
840
724
  def append_extra(
841
725
  self,
@@ -846,92 +730,87 @@ class Renderer(BaseRenderer):
846
730
  ) -> str:
847
731
  """
848
732
  Append extra data (images, files, etc.) to output
849
-
733
+
850
734
  :param meta: context meta
851
735
  :param ctx: context item
852
736
  :param footer: True if it is a footer
853
737
  :param render: True if render, False if only return HTML
854
738
  :return: HTML code
855
739
  """
856
- self.tool_output_end() # reset tools
857
- body = self.body
740
+ self.tool_output_end()
741
+
858
742
  pid = self.get_pid(meta)
859
- p = self.pids.get(pid)
860
- appended = []
861
- html = ""
862
- # images
743
+ appended = set()
744
+ html_parts = []
745
+
863
746
  c = len(ctx.images)
864
747
  if c > 0:
865
748
  n = 1
866
749
  for image in ctx.images:
867
750
  if image is None:
868
751
  continue
869
- # don't append if it is an external url
870
- # if image.startswith("http"):
871
- # continue
872
- if image in appended or image in p.images_appended:
752
+ if image in appended or image in self.pids[pid].images_appended:
873
753
  continue
874
754
  try:
875
- appended.append(image)
876
- html += body.get_image_html(image, n, c)
877
- p.images_appended.append(image)
755
+ appended.add(image)
756
+ html_parts.append(self.body.get_image_html(image, n, c))
757
+ self.pids[pid].images_appended.append(image)
878
758
  n += 1
879
759
  except Exception as e:
880
760
  pass
881
761
 
882
- # files and attachments, TODO check attachments
883
762
  c = len(ctx.files)
884
763
  if c > 0:
885
764
  files_html = []
886
765
  n = 1
887
766
  for file in ctx.files:
888
- if file in appended or file in p.files_appended:
767
+ if file in appended or file in self.pids[pid].files_appended:
889
768
  continue
890
769
  try:
891
- appended.append(file)
892
- files_html.append(body.get_file_html(file, n, c))
893
- p.files_appended.append(file)
770
+ appended.add(file)
771
+ files_html.append(self.body.get_file_html(file, n, c))
772
+ self.pids[pid].files_appended.append(file)
894
773
  n += 1
895
774
  except Exception as e:
896
775
  pass
897
776
  if files_html:
898
- html += "<br/>" + "<br/>".join(files_html)
777
+ html_parts.append("<br/><br/>".join(files_html))
899
778
 
900
- # urls
901
779
  c = len(ctx.urls)
902
780
  if c > 0:
903
781
  urls_html = []
904
782
  n = 1
905
783
  for url in ctx.urls:
906
- if url in appended or url in p.urls_appended:
784
+ if url in appended or url in self.pids[pid].urls_appended:
907
785
  continue
908
786
  try:
909
- appended.append(url)
910
- urls_html.append(body.get_url_html(url, n, c))
911
- p.urls_appended.append(url)
787
+ appended.add(url)
788
+ urls_html.append(self.body.get_url_html(url, n, c))
789
+ self.pids[pid].urls_appended.append(url)
912
790
  n += 1
913
791
  except Exception as e:
914
792
  pass
915
793
  if urls_html:
916
- html += "<br/>" + "<br/>".join(urls_html)
794
+ html_parts.append("<br/><br/>".join(urls_html))
917
795
 
918
- # docs json
919
796
  if self.window.core.config.get('ctx.sources'):
920
797
  if ctx.doc_ids is not None and len(ctx.doc_ids) > 0:
921
798
  try:
922
- docs = body.get_docs_html(ctx.doc_ids)
923
- html += docs
799
+ docs = self.body.get_docs_html(ctx.doc_ids)
800
+ html_parts.append(docs)
924
801
  except Exception as e:
925
802
  pass
926
- # flush
803
+
804
+ html = "".join(html_parts)
927
805
  if render and html != "":
928
806
  if footer:
929
- # append to output
930
807
  self.append(pid, html)
931
808
  else:
932
- # append to existing message box using JS
933
809
  escaped_html = json.dumps(html)
934
- self.get_output_node(meta).page().runJavaScript("appendExtra('{}',{});".format(ctx.id, escaped_html))
810
+ try:
811
+ self.get_output_node(meta).page().runJavaScript(f"appendExtra('{ctx.id}',{escaped_html});")
812
+ except Exception as e:
813
+ pass
935
814
 
936
815
  return html
937
816
 
@@ -958,7 +837,7 @@ class Renderer(BaseRenderer):
958
837
  if timestamp is not None:
959
838
  ts = datetime.fromtimestamp(timestamp)
960
839
  hour = ts.strftime("%H:%M:%S")
961
- text = '<span class="ts">{}: </span>{}'.format(hour, text)
840
+ text = f'<span class="ts">{hour}: </span>{text}'
962
841
  return text
963
842
 
964
843
  def reset(
@@ -971,16 +850,13 @@ class Renderer(BaseRenderer):
971
850
  :param meta: Context meta
972
851
  """
973
852
  pid = self.get_pid(meta)
974
- if pid is not None and pid in self.pids: # in PIDs only if at least one ctx item is appended
853
+ if pid is not None and pid in self.pids:
975
854
  self.reset_by_pid(pid)
976
855
  else:
977
- # there is no pid here if empty context so check for meta, and clear current
978
856
  if meta is not None:
979
- # create new PID using only meta
980
857
  pid = self.get_or_create_pid(meta)
981
858
  self.reset_by_pid(pid)
982
859
 
983
- # clear live output
984
860
  self.clear_live(meta, CtxItem())
985
861
 
986
862
  def reset_by_pid(self, pid: Optional[int]):
@@ -989,15 +865,19 @@ class Renderer(BaseRenderer):
989
865
 
990
866
  :param pid: context PID
991
867
  """
992
- p = self.pids.get(pid)
993
868
  self.parser.reset()
994
- p.clear(all=True)
869
+ self.pids[pid].item = None
870
+ self.pids[pid].html = ""
995
871
  self.clear_nodes(pid)
996
872
  self.clear_chunks(pid)
997
- self.get_output_node_by_pid(pid).reset_current_content()
873
+ self.pids[pid].images_appended = []
874
+ self.pids[pid].urls_appended = []
875
+ self.pids[pid].files_appended = []
876
+ node = self.get_output_node_by_pid(pid)
877
+ if node is not None:
878
+ node.reset_current_content()
998
879
  self.reset_names_by_pid(pid)
999
880
  self.prev_chunk_replace = False
1000
- self._stream_state.clear() # reset stream state
1001
881
 
1002
882
  def clear_input(self):
1003
883
  """Clear input"""
@@ -1038,8 +918,7 @@ class Renderer(BaseRenderer):
1038
918
  if pid is None:
1039
919
  return
1040
920
  if not self.pids[pid].loaded:
1041
- js = "var element = document.getElementById('_append_input_');"
1042
- js += "if (element) { element.innerHTML = ''; }"
921
+ js = "var element = document.getElementById('_append_input_');if (element) { element.innerHTML = ''; }"
1043
922
  else:
1044
923
  js = "clearInput();"
1045
924
  try:
@@ -1058,8 +937,7 @@ class Renderer(BaseRenderer):
1058
937
  """
1059
938
  self.prev_chunk_replace = False
1060
939
  if not self.pids[pid].loaded:
1061
- js = "var element = document.getElementById('_append_output_');"
1062
- js += "if (element) { element.innerHTML = ''; }"
940
+ js = "var element = document.getElementById('_append_output_');if (element) { element.innerHTML = ''; }"
1063
941
  else:
1064
942
  js = "clearOutput();"
1065
943
  try:
@@ -1077,8 +955,7 @@ class Renderer(BaseRenderer):
1077
955
  :pid: context PID
1078
956
  """
1079
957
  if not self.pids[pid].loaded:
1080
- js = "var element = document.getElementById('_nodes_');"
1081
- js += "if (element) { element.innerHTML = ''; }"
958
+ js = "var element = document.getElementById('_nodes_');if (element) { element.innerHTML = ''; }"
1082
959
  else:
1083
960
  js = "clearNodes();"
1084
961
  try:
@@ -1097,7 +974,7 @@ class Renderer(BaseRenderer):
1097
974
  ) -> str:
1098
975
  """
1099
976
  Prepare node HTML
1100
-
977
+
1101
978
  :param meta: context meta
1102
979
  :param ctx: CtxItem instance
1103
980
  :param html: html text
@@ -1142,48 +1019,42 @@ class Renderer(BaseRenderer):
1142
1019
  :param next_ctx: next context item
1143
1020
  :return: prepared HTML
1144
1021
  """
1145
- helpers = self.helpers
1146
- format_user_text = helpers.format_user_text
1147
- post_format_text = helpers.post_format_text
1148
- append_timestamp = self.append_timestamp
1149
- append_debug = self.append_debug
1150
- is_debug = self.is_debug
1151
- pids = self.pids
1152
- node_input = self.NODE_INPUT
1153
-
1154
- msg_id = f"msg-user-{ctx.id}" if ctx is not None else ""
1155
-
1156
- content = append_timestamp(
1022
+ msg_id = "msg-user-" + str(ctx.id) if ctx is not None else ""
1023
+ content = self.append_timestamp(
1157
1024
  ctx,
1158
- format_user_text(html),
1159
- type=node_input
1025
+ self.helpers.format_user_text(html),
1026
+ type=self.NODE_INPUT
1160
1027
  )
1161
- html = post_format_text(f"<p>{content}</p>")
1028
+ html = f"<p>{content}</p>"
1029
+ html = self.helpers.post_format_text(html)
1030
+ name = self.pids[pid].name_user
1162
1031
 
1163
- name = pids[pid].name_user
1164
1032
  if ctx.internal and ctx.input.startswith("[{"):
1165
1033
  name = trans("msg.name.system")
1166
1034
  if type(ctx.extra) is dict and "agent_evaluate" in ctx.extra:
1167
1035
  name = trans("msg.name.evaluation")
1168
1036
 
1169
- debug = append_debug(ctx, pid, "input") if is_debug() else ""
1037
+ debug = ""
1038
+ if self.is_debug():
1039
+ debug = self.append_debug(ctx, pid, "input")
1170
1040
 
1171
- extra = ""
1172
1041
  extra_style = ""
1042
+ extra = ""
1173
1043
  if ctx.extra is not None and "footer" in ctx.extra:
1174
1044
  extra = ctx.extra["footer"]
1175
1045
  extra_style = "display:block;"
1046
+ html = (
1047
+ f'<div class="msg-box msg-user" id="{msg_id}">'
1048
+ f'<div class="name-header name-user">{name}</div>'
1049
+ f'<div class="msg">'
1050
+ f'{html}'
1051
+ f'<div class="msg-extra" style="{extra_style}">{extra}</div>'
1052
+ f'{debug}'
1053
+ f'</div>'
1054
+ f'</div>'
1055
+ )
1176
1056
 
1177
- return "".join((
1178
- f'<div class="msg-box msg-user" id="{msg_id}">',
1179
- f'<div class="name-header name-user">{name}</div>',
1180
- '<div class="msg">',
1181
- html,
1182
- f'<div class="msg-extra" style="{extra_style}">{extra}</div>',
1183
- debug,
1184
- '</div>',
1185
- '</div>',
1186
- ))
1057
+ return html
1187
1058
 
1188
1059
  def prepare_node_output(
1189
1060
  self,
@@ -1203,97 +1074,86 @@ class Renderer(BaseRenderer):
1203
1074
  :param next_ctx: next context item
1204
1075
  :return: prepared HTML
1205
1076
  """
1206
- helpers = self.helpers
1207
- pre_format = helpers.pre_format_text
1208
- post_format = helpers.post_format_text
1209
- format_cmd = helpers.format_cmd_text
1210
- parser = self.parser
1211
- append_timestamp = self.append_timestamp
1212
- append_extra = self.append_extra
1213
- body = self.body
1214
- prepare_action_icons = body.prepare_action_icons
1215
- prepare_tool_extra = body.prepare_tool_extra
1216
- get_name_header = self.get_name_header
1217
- is_debug = self.is_debug
1218
- append_debug = self.append_debug
1219
- get_or_create_pid = self.get_or_create_pid
1220
- node_output = self.NODE_OUTPUT
1221
- trans_expand = trans('action.cmd.expand')
1222
-
1223
- str_id = str(ctx.id)
1224
- msg_id = f"msg-bot-{ctx.id}" if ctx is not None else ""
1225
- cmds_len = len(ctx.cmds)
1226
- extra_ctx_len = len(ctx.extra_ctx) if ctx.extra_ctx is not None else 0
1227
1077
  is_cmd = (
1228
- next_ctx is not None
1229
- and next_ctx.internal
1230
- and (cmds_len > 0 or extra_ctx_len > 0)
1231
- )
1232
-
1233
- pid = get_or_create_pid(meta)
1234
- html = pre_format(html)
1235
- html = parser.parse(html)
1236
- html = append_timestamp(ctx, html, type=node_output)
1237
- html = post_format(html)
1238
-
1239
- extra = append_extra(meta, ctx, footer=True, render=False)
1240
- footer = prepare_action_icons(ctx)
1241
-
1242
- app_path = self.window.core.config.get_app_path()
1243
- expand_icon_path = os.path.join(app_path, "data", "icons", "expand.svg")
1244
- cmd_icon = (
1245
- f'<img src="file://{expand_icon_path}" width="25" height="25" valign="middle">'
1246
- )
1247
- expand_btn = (
1248
- f"<span class='toggle-cmd-output' onclick='toggleToolOutput({str_id});' "
1249
- f"title='{trans_expand}' role='button'>{cmd_icon}</span>"
1078
+ next_ctx is not None and
1079
+ next_ctx.internal and
1080
+ (len(ctx.cmds) > 0 or (ctx.extra_ctx is not None and len(ctx.extra_ctx) > 0))
1250
1081
  )
1082
+ pid = self.get_or_create_pid(meta)
1083
+ msg_id = f"msg-bot-{ctx.id}" if ctx is not None else ""
1084
+ html = self.helpers.pre_format_text(html)
1085
+ html = self.parser.parse(html)
1086
+ html = self.append_timestamp(ctx, html, type=self.NODE_OUTPUT)
1087
+ html = self.helpers.post_format_text(html)
1088
+ extra = self.append_extra(meta, ctx, footer=True, render=False)
1089
+ footer = self.body.prepare_action_icons(ctx)
1251
1090
 
1252
1091
  tool_output = ""
1092
+ spinner = ""
1253
1093
  output_class = "display:none"
1254
- agent_step = (
1255
- ctx.results is not None
1256
- and len(ctx.results) > 0
1257
- and isinstance(ctx.extra, dict)
1258
- and "agent_step" in ctx.extra
1094
+ cmd_icon = f'<img src="{self._file_prefix}{self._icon_expand}" width="25" height="25" valign="middle">'
1095
+ expand_btn = (
1096
+ f"<span class='toggle-cmd-output' onclick='toggleToolOutput({ctx.id});' title='{trans('action.cmd.expand')}' "
1097
+ f"role='button'>{cmd_icon}</span>"
1259
1098
  )
1260
1099
 
1261
1100
  if is_cmd:
1262
- if agent_step:
1263
- tool_output = format_cmd(str(ctx.input))
1264
- output_class = "" # show tool output
1101
+ if ctx.results is not None and len(ctx.results) > 0 \
1102
+ and isinstance(ctx.extra, dict) and "agent_step" in ctx.extra:
1103
+ tool_output = self.helpers.format_cmd_text(str(ctx.input))
1104
+ output_class = ""
1265
1105
  else:
1266
- tool_output = format_cmd(str(next_ctx.input))
1267
- output_class = "" # show tool output
1268
- elif agent_step:
1269
- tool_output = format_cmd(str(ctx.input))
1270
-
1271
- html_tools = "".join((
1272
- f'<div class="tool-output" style="{output_class}">',
1273
- expand_btn,
1274
- '<div class="content" style="display:none">',
1275
- tool_output,
1276
- '</div></div>',
1277
- ))
1278
-
1279
- tool_extra = prepare_tool_extra(ctx)
1280
-
1281
- debug = append_debug(ctx, pid, "output") if is_debug() else ""
1282
- name_header = get_name_header(ctx)
1283
-
1284
- return "".join((
1285
- f'<div class="msg-box msg-bot" id="{msg_id}">',
1286
- name_header,
1287
- '<div class="msg">',
1288
- html,
1289
- f'<div class="msg-tool-extra">{tool_extra}</div>',
1290
- html_tools,
1291
- f'<div class="msg-extra">{extra}</div>',
1292
- footer,
1293
- debug,
1294
- '</div>',
1295
- '</div>',
1296
- ))
1106
+ tool_output = self.helpers.format_cmd_text(str(next_ctx.input))
1107
+ output_class = ""
1108
+
1109
+ elif ctx.results is not None and len(ctx.results) > 0 \
1110
+ and isinstance(ctx.extra, dict) and "agent_step" in ctx.extra:
1111
+ tool_output = self.helpers.format_cmd_text(str(ctx.input))
1112
+ else:
1113
+ out = (getattr(ctx, "output", "") or "")
1114
+ cmds = getattr(ctx, "cmds", ())
1115
+ if next_ctx is None and (
1116
+ cmds
1117
+ or out.startswith(('<tool>{"cmd"', '&lt;tool&gt;{"cmd"'))
1118
+ or out.rstrip().endswith(('}</tool>', '}&lt;/tool&gt;'))
1119
+ ):
1120
+ spinner_class = "" if ctx.live else "display:none"
1121
+ spinner = (
1122
+ f'<span class="spinner" style="{spinner_class}">'
1123
+ f'<img src="{self._file_prefix}{self._icon_sync}" width="30" height="30" '
1124
+ f'class="loading"></span>'
1125
+ )
1126
+
1127
+ html_tools = (
1128
+ f'<div class="tool-output" style="{output_class}">' +
1129
+ expand_btn +
1130
+ '<div class="content" style="display:none">' +
1131
+ tool_output +
1132
+ '</div></div>'
1133
+ )
1134
+ tool_extra = self.body.prepare_tool_extra(ctx)
1135
+
1136
+ debug = ""
1137
+ if self.is_debug():
1138
+ debug = self.append_debug(ctx, pid, "output")
1139
+
1140
+ name_header = self.get_name_header(ctx)
1141
+ html = (
1142
+ f'<div class="msg-box msg-bot" id="{msg_id}">' +
1143
+ name_header +
1144
+ '<div class="msg">' +
1145
+ f'{html}' +
1146
+ f'{spinner}' +
1147
+ f'<div class="msg-tool-extra">{tool_extra}</div>' +
1148
+ f'{html_tools}' +
1149
+ f'<div class="msg-extra">{extra}</div>' +
1150
+ f'{footer}' +
1151
+ f'{debug}' +
1152
+ '</div>' +
1153
+ '</div>'
1154
+ )
1155
+
1156
+ return html
1297
1157
 
1298
1158
  def get_name_header(self, ctx: CtxItem) -> str:
1299
1159
  """
@@ -1323,11 +1183,7 @@ class Renderer(BaseRenderer):
1323
1183
  avatars_dir = os.path.join(presets_dir, "avatars")
1324
1184
  avatar_path = os.path.join(avatars_dir, preset.ai_avatar)
1325
1185
  if os.path.exists(avatar_path):
1326
- if self.window.core.platforms.is_windows():
1327
- prefix = 'file:///'
1328
- else:
1329
- prefix = 'file://'
1330
- avatar_html = f"<img src=\"{prefix}{avatar_path}\" class=\"avatar\"> "
1186
+ avatar_html = f"<img src=\"{self._file_prefix}{avatar_path}\" class=\"avatar\"> "
1331
1187
 
1332
1188
  if not output_name and not avatar_html:
1333
1189
  return ""
@@ -1348,13 +1204,12 @@ class Renderer(BaseRenderer):
1348
1204
  try:
1349
1205
  node = self.get_output_node_by_pid(pid)
1350
1206
  node.page().runJavaScript(f"if (typeof window.appendNode !== 'undefined') appendNode({escaped_html});")
1351
- node.update_current_content()
1352
1207
  except Exception as e:
1353
1208
  pass
1354
1209
 
1355
1210
  def reload(self):
1356
1211
  """Reload output, called externally only on theme change to redraw content"""
1357
- self.window.controller.ctx.refresh_output() # if clear all and appends all items again
1212
+ self.window.controller.ctx.refresh_output()
1358
1213
 
1359
1214
  def flush(
1360
1215
  self,
@@ -1367,11 +1222,12 @@ class Renderer(BaseRenderer):
1367
1222
  :param pid: context PID
1368
1223
  """
1369
1224
  if self.pids[pid].loaded:
1370
- return # wait for page load
1225
+ return
1371
1226
 
1372
1227
  html = self.body.get_html(pid)
1373
- self.pids[pid].document = html
1374
- self.get_output_node_by_pid(pid).setHtml(html, baseUrl="file://")
1228
+ node = self.get_output_node_by_pid(pid)
1229
+ if node is not None:
1230
+ node.setHtml(html, baseUrl="file://")
1375
1231
 
1376
1232
  def fresh(
1377
1233
  self,
@@ -1385,14 +1241,16 @@ class Renderer(BaseRenderer):
1385
1241
  pid = self.get_or_create_pid(meta)
1386
1242
  if pid is None:
1387
1243
  return
1388
- p = self.pids[pid]
1389
1244
  html = self.body.get_html(pid)
1390
- p.clear(all=True)
1391
- p.loaded = False
1392
- p.document = html
1245
+ self.pids[pid].loaded = False
1393
1246
  node = self.get_output_node_by_pid(pid)
1394
- node.resetPage()
1395
- node.setHtml(html, baseUrl="file://")
1247
+ if node is not None:
1248
+ # hard reset
1249
+ # old_view = node
1250
+ # new_view = old_view.hard_reset()
1251
+ # self.window.ui.nodes['output'][pid] = new_view
1252
+ node.resetPage()
1253
+ node.setHtml(html, baseUrl="file://")
1396
1254
 
1397
1255
  def get_output_node(
1398
1256
  self,
@@ -1426,23 +1284,6 @@ class Renderer(BaseRenderer):
1426
1284
  """
1427
1285
  return self.window.ui.nodes['input']
1428
1286
 
1429
- def get_document(
1430
- self,
1431
- plain: bool = False
1432
- ):
1433
- """
1434
- Get document content (plain or HTML)
1435
-
1436
- :param plain: True to convert to plain text
1437
- :return: document content
1438
- """
1439
- pid = self.window.core.ctx.container.get_active_pid()
1440
- if pid is None:
1441
- return ""
1442
- if plain:
1443
- return self.parser.to_plain_text(self.pids[pid].document.replace("<br>", "\n"))
1444
- return self.pids[pid].document
1445
-
1446
1287
  def remove_item(self, ctx: CtxItem):
1447
1288
  """
1448
1289
  Remove item from output
@@ -1450,7 +1291,9 @@ class Renderer(BaseRenderer):
1450
1291
  :param ctx: context item
1451
1292
  """
1452
1293
  try:
1453
- self.get_output_node(ctx.meta).page().runJavaScript("if (typeof window.removeNode !== 'undefined') removeNode({});".format(ctx.id))
1294
+ _id = json.dumps(ctx.id)
1295
+ self.get_output_node(ctx.meta).page().runJavaScript(
1296
+ f"if (typeof window.removeNode !== 'undefined') removeNode({_id});")
1454
1297
  except Exception as e:
1455
1298
  pass
1456
1299
 
@@ -1461,7 +1304,9 @@ class Renderer(BaseRenderer):
1461
1304
  :param ctx: context item
1462
1305
  """
1463
1306
  try:
1464
- self.get_output_node(ctx.meta).page().runJavaScript("if (typeof window.removeNodesFromId !== 'undefined') removeNodesFromId({});".format(ctx.id))
1307
+ _id = json.dumps(ctx.id)
1308
+ self.get_output_node(ctx.meta).page().runJavaScript(
1309
+ f"if (typeof window.removeNodesFromId !== 'undefined') removeNodesFromId({_id});")
1465
1310
  except Exception as e:
1466
1311
  pass
1467
1312
 
@@ -1491,7 +1336,6 @@ class Renderer(BaseRenderer):
1491
1336
 
1492
1337
  :param ctx: context item
1493
1338
  """
1494
- # remove all items from ID
1495
1339
  self.remove_items_from(ctx)
1496
1340
 
1497
1341
  def on_edit_submit(self, ctx: CtxItem):
@@ -1500,7 +1344,6 @@ class Renderer(BaseRenderer):
1500
1344
 
1501
1345
  :param ctx: context item
1502
1346
  """
1503
- # remove all items from ID
1504
1347
  self.remove_items_from(ctx)
1505
1348
 
1506
1349
  def on_enable_edit(self, live: bool = True):
@@ -1512,7 +1355,8 @@ class Renderer(BaseRenderer):
1512
1355
  if not live:
1513
1356
  return
1514
1357
  try:
1515
- for node in self.get_all_nodes():
1358
+ nodes = self.get_all_nodes()
1359
+ for node in nodes:
1516
1360
  node.page().runJavaScript("if (typeof window.enableEditIcons !== 'undefined') enableEditIcons();")
1517
1361
  except Exception as e:
1518
1362
  pass
@@ -1526,7 +1370,8 @@ class Renderer(BaseRenderer):
1526
1370
  if not live:
1527
1371
  return
1528
1372
  try:
1529
- for node in self.get_all_nodes():
1373
+ nodes = self.get_all_nodes()
1374
+ for node in nodes:
1530
1375
  node.page().runJavaScript("if (typeof window.disableEditIcons !== 'undefined') disableEditIcons();")
1531
1376
  except Exception as e:
1532
1377
  pass
@@ -1540,7 +1385,8 @@ class Renderer(BaseRenderer):
1540
1385
  if not live:
1541
1386
  return
1542
1387
  try:
1543
- for node in self.get_all_nodes():
1388
+ nodes = self.get_all_nodes()
1389
+ for node in nodes:
1544
1390
  node.page().runJavaScript("if (typeof window.enableTimestamp !== 'undefined') enableTimestamp();")
1545
1391
  except Exception as e:
1546
1392
  pass
@@ -1554,7 +1400,8 @@ class Renderer(BaseRenderer):
1554
1400
  if not live:
1555
1401
  return
1556
1402
  try:
1557
- for node in self.get_all_nodes():
1403
+ nodes = self.get_all_nodes()
1404
+ for node in nodes:
1558
1405
  node.page().runJavaScript("if (typeof window.disableTimestamp !== 'undefined') disableTimestamp();")
1559
1406
  except Exception as e:
1560
1407
  pass
@@ -1566,7 +1413,7 @@ class Renderer(BaseRenderer):
1566
1413
  ):
1567
1414
  """
1568
1415
  Update names
1569
-
1416
+
1570
1417
  :param meta: context meta
1571
1418
  :param ctx: context item
1572
1419
  """
@@ -1580,7 +1427,7 @@ class Renderer(BaseRenderer):
1580
1427
 
1581
1428
  def clear_all(self):
1582
1429
  """Clear all"""
1583
- for pid in self.pids.keys():
1430
+ for pid in self.pids:
1584
1431
  self.clear_chunks(pid)
1585
1432
  self.clear_nodes(pid)
1586
1433
  self.pids[pid].html = ""
@@ -1614,15 +1461,18 @@ class Renderer(BaseRenderer):
1614
1461
  def reload_css(self):
1615
1462
  """Reload CSS - all, global"""
1616
1463
  to_json = json.dumps(self.body.prepare_styles())
1617
- for pid in self.pids.keys():
1464
+ nodes = self.get_all_nodes()
1465
+ for pid in self.pids:
1618
1466
  if self.pids[pid].loaded:
1619
- for node in self.get_all_nodes():
1467
+ for node in nodes:
1620
1468
  try:
1621
- node.page().runJavaScript("if (typeof window.updateCSS !== 'undefined') updateCSS({});".format(to_json))
1469
+ node.page().runJavaScript(
1470
+ f"if (typeof window.updateCSS !== 'undefined') updateCSS({to_json});")
1622
1471
  if self.window.core.config.get('render.blocks'):
1623
1472
  node.page().runJavaScript("if (typeof window.enableBlocks !== 'undefined') enableBlocks();")
1624
1473
  else:
1625
- node.page().runJavaScript("if (typeof window.disableBlocks !== 'undefined') disableBlocks();") # TODO: ctx!!!!!
1474
+ node.page().runJavaScript(
1475
+ "if (typeof window.disableBlocks !== 'undefined') disableBlocks();")
1626
1476
  except Exception as e:
1627
1477
  pass
1628
1478
  return
@@ -1630,7 +1480,7 @@ class Renderer(BaseRenderer):
1630
1480
  def on_theme_change(self):
1631
1481
  """On theme change"""
1632
1482
  self.window.controller.theme.markdown.load()
1633
- for pid in self.pids.keys():
1483
+ for pid in self.pids:
1634
1484
  if self.pids[pid].loaded:
1635
1485
  self.reload_css()
1636
1486
  return
@@ -1648,7 +1498,8 @@ class Renderer(BaseRenderer):
1648
1498
  """
1649
1499
  escaped_content = json.dumps(content)
1650
1500
  try:
1651
- self.get_output_node(meta).page().runJavaScript(f"if (typeof window.appendToolOutput !== 'undefined') appendToolOutput({escaped_content});")
1501
+ self.get_output_node(meta).page().runJavaScript(
1502
+ f"if (typeof window.appendToolOutput !== 'undefined') appendToolOutput({escaped_content});")
1652
1503
  except Exception as e:
1653
1504
  pass
1654
1505
 
@@ -1665,7 +1516,8 @@ class Renderer(BaseRenderer):
1665
1516
  """
1666
1517
  escaped_content = json.dumps(content)
1667
1518
  try:
1668
- self.get_output_node(meta).page().runJavaScript(f"if (typeof window.updateToolOutput !== 'undefined') updateToolOutput({escaped_content});")
1519
+ self.get_output_node(meta).page().runJavaScript(
1520
+ f"if (typeof window.updateToolOutput !== 'undefined') updateToolOutput({escaped_content});")
1669
1521
  except Exception as e:
1670
1522
  pass
1671
1523
 
@@ -1676,7 +1528,8 @@ class Renderer(BaseRenderer):
1676
1528
  :param meta: context meta
1677
1529
  """
1678
1530
  try:
1679
- self.get_output_node(meta).page().runJavaScript(f"if (typeof window.clearToolOutput !== 'undefined') clearToolOutput();")
1531
+ self.get_output_node(meta).page().runJavaScript(
1532
+ f"if (typeof window.clearToolOutput !== 'undefined') clearToolOutput();")
1680
1533
  except Exception as e:
1681
1534
  pass
1682
1535
 
@@ -1687,14 +1540,16 @@ class Renderer(BaseRenderer):
1687
1540
  :param meta: context meta
1688
1541
  """
1689
1542
  try:
1690
- self.get_output_node(meta).page().runJavaScript(f"if (typeof window.beginToolOutput !== 'undefined') beginToolOutput();")
1543
+ self.get_output_node(meta).page().runJavaScript(
1544
+ f"if (typeof window.beginToolOutput !== 'undefined') beginToolOutput();")
1691
1545
  except Exception as e:
1692
1546
  pass
1693
1547
 
1694
1548
  def tool_output_end(self):
1695
1549
  """End tool output"""
1696
1550
  try:
1697
- self.get_output_node().page().runJavaScript(f"if (typeof window.endToolOutput !== 'undefined') endToolOutput();")
1551
+ self.get_output_node().page().runJavaScript(
1552
+ f"if (typeof window.endToolOutput !== 'undefined') endToolOutput();")
1698
1553
  except Exception as e:
1699
1554
  pass
1700
1555
 
@@ -1713,7 +1568,8 @@ class Renderer(BaseRenderer):
1713
1568
  """
1714
1569
  if title is None:
1715
1570
  title = "debug"
1716
- return f"<div class='debug'><b>{title}:</b> pid: {pid}, ctx: {html.escape(ctx.to_debug())}</div>"
1571
+ debug = "<b>" + title + ":</b> pid: " + str(pid) + ", ctx: " + str(ctx.to_dict())
1572
+ return "<div class='debug'>" + debug + "</div>"
1717
1573
 
1718
1574
  def is_debug(self) -> bool:
1719
1575
  """
@@ -1728,4 +1584,4 @@ class Renderer(BaseRenderer):
1728
1584
  Remove PID from renderer
1729
1585
  """
1730
1586
  if pid in self.pids:
1731
- del self.pids[pid]
1587
+ del self.pids[pid]