pygpt-net 2.6.10__py3-none-any.whl → 2.6.12__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 (46) hide show
  1. pygpt_net/CHANGELOG.txt +10 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +7 -1
  4. pygpt_net/config.py +11 -11
  5. pygpt_net/controller/access/access.py +49 -2
  6. pygpt_net/controller/chat/attachment.py +13 -13
  7. pygpt_net/controller/chat/command.py +4 -4
  8. pygpt_net/controller/chat/common.py +9 -14
  9. pygpt_net/controller/chat/files.py +2 -2
  10. pygpt_net/controller/chat/input.py +4 -4
  11. pygpt_net/controller/chat/output.py +4 -4
  12. pygpt_net/controller/chat/render.py +11 -6
  13. pygpt_net/controller/chat/response.py +7 -7
  14. pygpt_net/controller/chat/stream.py +11 -6
  15. pygpt_net/controller/chat/text.py +15 -10
  16. pygpt_net/controller/command/command.py +7 -7
  17. pygpt_net/controller/ctx/ctx.py +9 -5
  18. pygpt_net/controller/debug/debug.py +2 -2
  19. pygpt_net/core/ctx/bag.py +2 -1
  20. pygpt_net/core/debug/debug.py +17 -3
  21. pygpt_net/core/dispatcher/dispatcher.py +5 -3
  22. pygpt_net/core/events/render.py +3 -0
  23. pygpt_net/core/render/base.py +4 -4
  24. pygpt_net/core/render/web/body.py +83 -88
  25. pygpt_net/core/render/web/parser.py +11 -6
  26. pygpt_net/core/render/web/pid.py +19 -4
  27. pygpt_net/core/render/web/renderer.py +217 -74
  28. pygpt_net/data/config/config.json +3 -3
  29. pygpt_net/data/config/models.json +3 -3
  30. pygpt_net/data/config/presets/agent_openai.json +1 -1
  31. pygpt_net/data/config/presets/agent_openai_assistant.json +1 -1
  32. pygpt_net/data/config/presets/agent_planner.json +1 -1
  33. pygpt_net/data/config/presets/agent_react.json +1 -1
  34. pygpt_net/item/ctx.py +3 -3
  35. pygpt_net/launcher.py +2 -9
  36. pygpt_net/provider/gpt/__init__.py +13 -4
  37. pygpt_net/tools/code_interpreter/body.py +2 -3
  38. pygpt_net/ui/main.py +8 -3
  39. pygpt_net/ui/widget/textarea/html.py +2 -7
  40. pygpt_net/ui/widget/textarea/web.py +52 -28
  41. pygpt_net/utils.py +15 -8
  42. {pygpt_net-2.6.10.dist-info → pygpt_net-2.6.12.dist-info}/METADATA +12 -2
  43. {pygpt_net-2.6.10.dist-info → pygpt_net-2.6.12.dist-info}/RECORD +46 -46
  44. {pygpt_net-2.6.10.dist-info → pygpt_net-2.6.12.dist-info}/LICENSE +0 -0
  45. {pygpt_net-2.6.10.dist-info → pygpt_net-2.6.12.dist-info}/WHEEL +0 -0
  46. {pygpt_net-2.6.10.dist-info → pygpt_net-2.6.12.dist-info}/entry_points.txt +0 -0
@@ -6,14 +6,16 @@
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.16 00:00:00 #
9
+ # Updated Date: 2025.08.19 07:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import json
13
13
  import os
14
14
  import re
15
+
15
16
  from datetime import datetime
16
17
  from typing import Optional, List, Any
18
+ from time import monotonic
17
19
 
18
20
  from pygpt_net.core.render.base import BaseRenderer
19
21
  from pygpt_net.core.text.utils import has_unclosed_code_tag
@@ -31,6 +33,7 @@ from pygpt_net.core.events import RenderEvent
31
33
 
32
34
 
33
35
  class Renderer(BaseRenderer):
36
+
34
37
  NODE_INPUT = 0
35
38
  NODE_OUTPUT = 1
36
39
  ENDINGS_CODE = (
@@ -43,6 +46,7 @@ class Renderer(BaseRenderer):
43
46
  "</ol>",
44
47
  "</li>"
45
48
  )
49
+ RE_AMP_LT_GT = re.compile(r'&amp;(lt|gt);')
46
50
 
47
51
  def __init__(self, window=None):
48
52
  super(Renderer, self).__init__(window)
@@ -55,7 +59,7 @@ class Renderer(BaseRenderer):
55
59
  self.body = Body(window)
56
60
  self.helpers = Helpers(window)
57
61
  self.parser = Parser(window)
58
- self.pids = {} # per node data
62
+ self.pids = {}
59
63
  self.prev_chunk_replace = False
60
64
  self.prev_chunk_newline = False
61
65
 
@@ -64,6 +68,9 @@ class Renderer(BaseRenderer):
64
68
  self._icon_sync = os.path.join(app_path, "data", "icons", "sync.svg")
65
69
  self._file_prefix = 'file:///' if self.window and self.window.core.platforms.is_windows() else 'file://'
66
70
 
71
+ self._thr = {}
72
+ self._throttle_interval = 0.01 # 10 ms delay
73
+
67
74
  def prepare(self):
68
75
  """
69
76
  Prepare renderer
@@ -82,7 +89,7 @@ class Renderer(BaseRenderer):
82
89
  self.parser.reset()
83
90
  try:
84
91
  node.page().runJavaScript("if (typeof window.prepare !== 'undefined') prepare();")
85
- except Exception as e:
92
+ except Exception:
86
93
  pass
87
94
 
88
95
  def on_page_loaded(
@@ -102,8 +109,6 @@ class Renderer(BaseRenderer):
102
109
  if pid is None or pid not in self.pids:
103
110
  return
104
111
  self.pids[pid].loaded = True
105
- node = self.get_output_node(meta)
106
-
107
112
  if self.pids[pid].html != "" and not self.pids[pid].use_buffer:
108
113
  self.clear_chunks_input(pid)
109
114
  self.clear_chunks_output(pid)
@@ -111,8 +116,6 @@ class Renderer(BaseRenderer):
111
116
  self.append(pid, self.pids[pid].html, flush=True)
112
117
  self.pids[pid].html = ""
113
118
 
114
- node.setUpdatesEnabled(True)
115
-
116
119
  def get_pid(self, meta: CtxMeta):
117
120
  """
118
121
  Get PID for context meta
@@ -279,7 +282,7 @@ class Renderer(BaseRenderer):
279
282
  self.prev_chunk_replace = False
280
283
  try:
281
284
  self.get_output_node(meta).page().runJavaScript("beginStream();")
282
- except Exception as e:
285
+ except Exception:
283
286
  pass
284
287
 
285
288
  def stream_end(
@@ -297,6 +300,9 @@ class Renderer(BaseRenderer):
297
300
  pid = self.get_or_create_pid(meta)
298
301
  if pid is None:
299
302
  return
303
+
304
+ self._throttle_emit(pid, force=True)
305
+ self._throttle_reset(pid)
300
306
  if self.window.controller.agent.legacy.enabled():
301
307
  if self.pids[pid].item is not None:
302
308
  self.append_context_item(meta, self.pids[pid].item)
@@ -304,7 +310,7 @@ class Renderer(BaseRenderer):
304
310
  self.pids[pid].clear()
305
311
  try:
306
312
  self.get_output_node(meta).page().runJavaScript("endStream();")
307
- except Exception as e:
313
+ except Exception:
308
314
  pass
309
315
 
310
316
  def append_context(
@@ -333,6 +339,7 @@ class Renderer(BaseRenderer):
333
339
  self.pids[pid].use_buffer = True
334
340
  self.pids[pid].html = ""
335
341
  prev_ctx = None
342
+ next_item = None
336
343
  total = len(items)
337
344
  for i, item in enumerate(items):
338
345
  self.update_names(meta, item)
@@ -347,14 +354,17 @@ class Renderer(BaseRenderer):
347
354
  next_ctx=next_item
348
355
  )
349
356
  prev_ctx = item
350
- self.pids[pid].use_buffer = False
351
357
 
358
+ prev_ctx = None
359
+ next_item = None
360
+ self.pids[pid].use_buffer = False
352
361
  if self.pids[pid].html != "":
353
362
  self.append(
354
363
  pid,
355
364
  self.pids[pid].html,
356
365
  flush=True
357
366
  )
367
+ self.parser.reset()
358
368
 
359
369
  def append_input(
360
370
  self, meta: CtxMeta,
@@ -465,6 +475,8 @@ class Renderer(BaseRenderer):
465
475
  if not text_chunk:
466
476
  if begin:
467
477
  pctx.clear()
478
+ self._throttle_emit(pid, force=True)
479
+ self._throttle_reset(pid)
468
480
  return
469
481
 
470
482
  name_header_str = self.get_name_header(ctx)
@@ -477,8 +489,10 @@ class Renderer(BaseRenderer):
477
489
  debug = self.append_debug(ctx, pid, "stream")
478
490
  if debug:
479
491
  text_chunk = debug + text_chunk
480
- pctx.clear() # reset buffer
481
- pctx.is_cmd = False # reset command flag
492
+ self._throttle_emit(pid, force=True)
493
+ self._throttle_reset(pid)
494
+ pctx.clear()
495
+ pctx.is_cmd = False
482
496
  self.clear_chunks_output(pid)
483
497
  self.prev_chunk_replace = False
484
498
 
@@ -491,53 +505,43 @@ class Renderer(BaseRenderer):
491
505
  buffer_to_parse = buffer
492
506
 
493
507
  html = self.parser.parse(buffer_to_parse)
508
+ del buffer_to_parse
494
509
  is_code_block = html.endswith(self.ENDINGS_CODE)
495
510
  is_list = html.endswith(self.ENDINGS_LIST)
496
- is_newline = ("\n" in text_chunk) or buffer.endswith("\n") or is_code_block
511
+ is_n = "\n" in text_chunk
512
+ is_newline = is_n or buffer.endswith("\n") or is_code_block
497
513
  force_replace = False
498
514
  if self.prev_chunk_newline:
499
515
  force_replace = True
500
- if "\n" in text_chunk:
516
+ if is_n:
501
517
  self.prev_chunk_newline = True
502
518
  else:
503
519
  self.prev_chunk_newline = False
504
520
 
505
- replace_bool = False
521
+ replace = False
506
522
  if is_newline or force_replace or is_list:
507
- replace_bool = True
523
+ replace = True
508
524
  if is_code_block:
509
- # don't replace if it is a code block
510
- if "\n" not in text_chunk:
511
- # if there is no newline in raw_chunk, then don't replace
512
- replace_bool = False
525
+ if not is_n:
526
+ replace = False
513
527
 
514
528
  if not is_code_block:
515
- text_chunk = text_chunk.replace("\n", "<br/>")
529
+ if is_n:
530
+ text_chunk = text_chunk.replace("\n", "<br/>")
516
531
  else:
517
- if self.prev_chunk_replace and not has_unclosed_code_tag(text_chunk):
518
- # if previous chunk was replaced and current is code block, then add \n to chunk
519
- text_chunk = "".join(("\n", text_chunk)) # add newline to chunk
532
+ if self.prev_chunk_replace and (is_code_block and not has_unclosed_code_tag(text_chunk)):
533
+ text_chunk = "\n" + text_chunk
520
534
 
521
- self.prev_chunk_replace = replace_bool
535
+ self.prev_chunk_replace = replace
522
536
 
523
- # hide loading spinner if it is the beginning of the text
524
537
  if begin:
525
538
  try:
526
539
  self.get_output_node(meta).page().runJavaScript("hideLoading();")
527
- except Exception as e:
540
+ except Exception:
528
541
  pass
529
542
 
530
- # emit chunk to output node
531
- try:
532
- self.get_output_node(meta).page().bridge.chunk.emit(
533
- name_header_str or "",
534
- html if replace_bool else "",
535
- text_chunk if not replace_bool else "",
536
- bool(replace_bool),
537
- bool(is_code_block),
538
- )
539
- except Exception as e:
540
- pass
543
+ self._throttle_queue(pid, name_header_str or "", html, text_chunk, replace, bool(is_code_block))
544
+ self._throttle_emit(pid, force=False)
541
545
 
542
546
  def next_chunk(
543
547
  self,
@@ -551,6 +555,8 @@ class Renderer(BaseRenderer):
551
555
  :param ctx: context item
552
556
  """
553
557
  pid = self.get_or_create_pid(meta)
558
+ self._throttle_emit(pid, force=True)
559
+ self._throttle_reset(pid)
554
560
  self.pids[pid].item = ctx
555
561
  self.pids[pid].buffer = ""
556
562
  self.update_names(meta, ctx)
@@ -558,8 +564,9 @@ class Renderer(BaseRenderer):
558
564
  self.prev_chunk_newline = False
559
565
  try:
560
566
  self.get_output_node(meta).page().runJavaScript(
561
- "nextStream();")
562
- except Exception as e:
567
+ "nextStream();"
568
+ )
569
+ except Exception:
563
570
  pass
564
571
 
565
572
  def append_chunk_input(
@@ -586,9 +593,13 @@ class Renderer(BaseRenderer):
586
593
  self.clear_chunks_input(pid)
587
594
  try:
588
595
  self.get_output_node(meta).page().runJavaScript(
589
- f"appendToInput({self.to_json(self.helpers.format_chunk(text_chunk))});"
596
+ f"""appendToInput({self.to_json(
597
+ self.sanitize_html(
598
+ self.helpers.format_chunk(text_chunk)
599
+ )
600
+ )});"""
590
601
  )
591
- except Exception as e:
602
+ except Exception:
592
603
  pass
593
604
 
594
605
  def append_live(
@@ -630,9 +641,14 @@ class Renderer(BaseRenderer):
630
641
  to_append += "\n```"
631
642
  try:
632
643
  self.get_output_node(meta).page().runJavaScript(
633
- f"replaceLive({self.to_json(self.parser.parse(to_append))});"
644
+ f"""replaceLive({self.to_json(
645
+ self.sanitize_html(
646
+ self.parser.parse(to_append)
647
+ )
648
+ )});"""
634
649
  )
635
650
  except Exception as e:
651
+ print(e)
636
652
  pass
637
653
 
638
654
  def clear_live(self, meta: CtxMeta, ctx: CtxItem):
@@ -651,7 +667,7 @@ class Renderer(BaseRenderer):
651
667
  js = "clearLive();"
652
668
  try:
653
669
  self.get_output_node_by_pid(pid).page().runJavaScript(js)
654
- except Exception as e:
670
+ except Exception:
655
671
  pass
656
672
 
657
673
  def append_node(
@@ -704,8 +720,9 @@ class Renderer(BaseRenderer):
704
720
  """
705
721
  if self.pids[pid].loaded and not self.pids[pid].use_buffer:
706
722
  self.clear_chunks(pid)
707
- self.flush_output(pid, html)
708
- self.pids[pid].html = ""
723
+ if html:
724
+ self.flush_output(pid, html)
725
+ self.pids[pid].clear()
709
726
  else:
710
727
  if not flush:
711
728
  self.pids[pid].append_html(html)
@@ -824,7 +841,11 @@ class Renderer(BaseRenderer):
824
841
  self.append(pid, html)
825
842
  else:
826
843
  try:
827
- self.get_output_node(meta).page().runJavaScript(f"appendExtra('{ctx.id}',{self.to_json(html)});")
844
+ self.get_output_node(meta).page().runJavaScript(
845
+ f"""appendExtra('{ctx.id}',{self.to_json(
846
+ self.sanitize_html(html)
847
+ )});"""
848
+ )
828
849
  except Exception as e:
829
850
  pass
830
851
 
@@ -894,6 +915,7 @@ class Renderer(BaseRenderer):
894
915
  node.reset_current_content()
895
916
  self.reset_names_by_pid(pid)
896
917
  self.prev_chunk_replace = False
918
+ self._throttle_reset(pid)
897
919
 
898
920
  def clear_input(self):
899
921
  """Clear input"""
@@ -939,7 +961,7 @@ class Renderer(BaseRenderer):
939
961
  js = "clearInput();"
940
962
  try:
941
963
  self.get_output_node_by_pid(pid).page().runJavaScript(js)
942
- except Exception as e:
964
+ except Exception:
943
965
  pass
944
966
 
945
967
  def clear_chunks_output(
@@ -958,8 +980,9 @@ class Renderer(BaseRenderer):
958
980
  js = "clearOutput();"
959
981
  try:
960
982
  self.get_output_node_by_pid(pid).page().runJavaScript(js)
961
- except Exception as e:
983
+ except Exception:
962
984
  pass
985
+ self._throttle_reset(pid)
963
986
 
964
987
  def clear_nodes(
965
988
  self,
@@ -976,7 +999,7 @@ class Renderer(BaseRenderer):
976
999
  js = "clearNodes();"
977
1000
  try:
978
1001
  self.get_output_node_by_pid(pid).page().runJavaScript(js)
979
- except Exception as e:
1002
+ except Exception:
980
1003
  pass
981
1004
 
982
1005
  def prepare_node(
@@ -1174,10 +1197,11 @@ class Renderer(BaseRenderer):
1174
1197
  """
1175
1198
  try:
1176
1199
  self.get_output_node_by_pid(pid).page().runJavaScript(
1177
- f"if (typeof window.appendNode !== 'undefined') appendNode({self.to_json(html)});"
1200
+ f"""if (typeof window.appendNode !== 'undefined') appendNode({self.to_json(self.sanitize_html(html))});"""
1178
1201
  )
1179
- except Exception as e:
1202
+ except Exception:
1180
1203
  pass
1204
+ html = None
1181
1205
 
1182
1206
  def reload(self):
1183
1207
  """Reload output, called externally only on theme change to redraw content"""
@@ -1213,17 +1237,10 @@ class Renderer(BaseRenderer):
1213
1237
  pid = self.get_or_create_pid(meta)
1214
1238
  if pid is None:
1215
1239
  return
1216
- html = self.body.get_html(pid)
1217
- self.pids[pid].loaded = False
1218
1240
  node = self.get_output_node_by_pid(pid)
1219
1241
  if node is not None:
1220
- # hard reset
1221
- # old_view = node
1222
- # new_view = old_view.hard_reset()
1223
- # self.window.ui.nodes['output'][pid] = new_view
1224
1242
  node.resetPage()
1225
- node.setHtml(html, baseUrl="file://")
1226
- self.pids[pid].html = ""
1243
+ self._throttle_reset(pid)
1227
1244
 
1228
1245
  def get_output_node(
1229
1246
  self,
@@ -1266,7 +1283,7 @@ class Renderer(BaseRenderer):
1266
1283
  try:
1267
1284
  self.get_output_node(ctx.meta).page().runJavaScript(
1268
1285
  f"if (typeof window.removeNode !== 'undefined') removeNode({self.to_json(ctx.id)});")
1269
- except Exception as e:
1286
+ except Exception:
1270
1287
  pass
1271
1288
 
1272
1289
  def remove_items_from(self, ctx: CtxItem):
@@ -1278,7 +1295,7 @@ class Renderer(BaseRenderer):
1278
1295
  try:
1279
1296
  self.get_output_node(ctx.meta).page().runJavaScript(
1280
1297
  f"if (typeof window.removeNodesFromId !== 'undefined') removeNodesFromId({self.to_json(ctx.id)});")
1281
- except Exception as e:
1298
+ except Exception:
1282
1299
  pass
1283
1300
 
1284
1301
  def reset_names(self, meta: CtxMeta):
@@ -1329,7 +1346,7 @@ class Renderer(BaseRenderer):
1329
1346
  nodes = self.get_all_nodes()
1330
1347
  for node in nodes:
1331
1348
  node.page().runJavaScript("if (typeof window.enableEditIcons !== 'undefined') enableEditIcons();")
1332
- except Exception as e:
1349
+ except Exception:
1333
1350
  pass
1334
1351
 
1335
1352
  def on_disable_edit(self, live: bool = True):
@@ -1344,7 +1361,7 @@ class Renderer(BaseRenderer):
1344
1361
  nodes = self.get_all_nodes()
1345
1362
  for node in nodes:
1346
1363
  node.page().runJavaScript("if (typeof window.disableEditIcons !== 'undefined') disableEditIcons();")
1347
- except Exception as e:
1364
+ except Exception:
1348
1365
  pass
1349
1366
 
1350
1367
  def on_enable_timestamp(self, live: bool = True):
@@ -1359,7 +1376,7 @@ class Renderer(BaseRenderer):
1359
1376
  nodes = self.get_all_nodes()
1360
1377
  for node in nodes:
1361
1378
  node.page().runJavaScript("if (typeof window.enableTimestamp !== 'undefined') enableTimestamp();")
1362
- except Exception as e:
1379
+ except Exception:
1363
1380
  pass
1364
1381
 
1365
1382
  def on_disable_timestamp(self, live: bool = True):
@@ -1374,7 +1391,7 @@ class Renderer(BaseRenderer):
1374
1391
  nodes = self.get_all_nodes()
1375
1392
  for node in nodes:
1376
1393
  node.page().runJavaScript("if (typeof window.disableTimestamp !== 'undefined') disableTimestamp();")
1377
- except Exception as e:
1394
+ except Exception:
1378
1395
  pass
1379
1396
 
1380
1397
  def update_names(
@@ -1402,6 +1419,7 @@ class Renderer(BaseRenderer):
1402
1419
  self.clear_chunks(pid)
1403
1420
  self.clear_nodes(pid)
1404
1421
  self.pids[pid].html = ""
1422
+ self._throttle_reset(pid)
1405
1423
 
1406
1424
  def scroll_to_bottom(self):
1407
1425
  """Scroll to bottom"""
@@ -1469,9 +1487,11 @@ class Renderer(BaseRenderer):
1469
1487
  """
1470
1488
  try:
1471
1489
  self.get_output_node(meta).page().runJavaScript(
1472
- f"if (typeof window.appendToolOutput !== 'undefined') appendToolOutput({self.to_json(content)});"
1490
+ f"""if (typeof window.appendToolOutput !== 'undefined') appendToolOutput({self.to_json(
1491
+ self.sanitize_html(content)
1492
+ )});"""
1473
1493
  )
1474
- except Exception as e:
1494
+ except Exception:
1475
1495
  pass
1476
1496
 
1477
1497
  def tool_output_update(
@@ -1487,9 +1507,11 @@ class Renderer(BaseRenderer):
1487
1507
  """
1488
1508
  try:
1489
1509
  self.get_output_node(meta).page().runJavaScript(
1490
- f"if (typeof window.updateToolOutput !== 'undefined') updateToolOutput({self.to_json(content)});"
1510
+ f"""if (typeof window.updateToolOutput !== 'undefined') updateToolOutput({self.to_json(
1511
+ self.sanitize_html(content)
1512
+ )});"""
1491
1513
  )
1492
- except Exception as e:
1514
+ except Exception:
1493
1515
  pass
1494
1516
 
1495
1517
  def tool_output_clear(self, meta: CtxMeta):
@@ -1502,7 +1524,7 @@ class Renderer(BaseRenderer):
1502
1524
  self.get_output_node(meta).page().runJavaScript(
1503
1525
  f"if (typeof window.clearToolOutput !== 'undefined') clearToolOutput();"
1504
1526
  )
1505
- except Exception as e:
1527
+ except Exception:
1506
1528
  pass
1507
1529
 
1508
1530
  def tool_output_begin(self, meta: CtxMeta):
@@ -1515,7 +1537,7 @@ class Renderer(BaseRenderer):
1515
1537
  self.get_output_node(meta).page().runJavaScript(
1516
1538
  f"if (typeof window.beginToolOutput !== 'undefined') beginToolOutput();"
1517
1539
  )
1518
- except Exception as e:
1540
+ except Exception:
1519
1541
  pass
1520
1542
 
1521
1543
  def tool_output_end(self):
@@ -1524,9 +1546,20 @@ class Renderer(BaseRenderer):
1524
1546
  self.get_output_node().page().runJavaScript(
1525
1547
  f"if (typeof window.endToolOutput !== 'undefined') endToolOutput();"
1526
1548
  )
1527
- except Exception as e:
1549
+ except Exception:
1528
1550
  pass
1529
1551
 
1552
+ def sanitize_html(self, html: str) -> str:
1553
+ """
1554
+ Sanitize HTML to prevent XSS attacks
1555
+
1556
+ :param html: HTML string to sanitize
1557
+ :return: sanitized HTML string
1558
+ """
1559
+ if not html:
1560
+ return ""
1561
+ return self.RE_AMP_LT_GT.sub(r'&\1;', html)
1562
+
1530
1563
  def append_debug(
1531
1564
  self,
1532
1565
  ctx: CtxItem,
@@ -1539,6 +1572,7 @@ class Renderer(BaseRenderer):
1539
1572
  :param ctx: context item
1540
1573
  :param pid: context PID
1541
1574
  :param title: debug title
1575
+ :return: HTML debug info
1542
1576
  """
1543
1577
  if title is None:
1544
1578
  title = "debug"
@@ -1555,6 +1589,115 @@ class Renderer(BaseRenderer):
1555
1589
  def remove_pid(self, pid: int):
1556
1590
  """
1557
1591
  Remove PID from renderer
1592
+
1593
+ :param pid: context PID
1558
1594
  """
1559
1595
  if pid in self.pids:
1560
- del self.pids[pid]
1596
+ del self.pids[pid]
1597
+ self._thr.pop(pid, None)
1598
+
1599
+ def _throttle_get(self, pid: int) -> dict:
1600
+ """
1601
+ Return per-pid throttle state
1602
+
1603
+ :param pid: context PID
1604
+ :return: throttle state dictionary
1605
+ """
1606
+ thr = self._thr.get(pid)
1607
+ if thr is None:
1608
+ thr = {"last": 0.0, "op": 0, "name": "", "replace_html": "", "append": [], "code": False}
1609
+ self._thr[pid] = thr
1610
+ return thr
1611
+
1612
+ def _throttle_reset(self, pid: Optional[int]):
1613
+ """
1614
+ Reset throttle state
1615
+
1616
+ :param pid: context PID
1617
+ """
1618
+ if pid is None:
1619
+ return
1620
+ thr = self._thr.get(pid)
1621
+ if thr is None:
1622
+ return
1623
+ thr["op"] = 0
1624
+ thr["name"] = ""
1625
+ thr["replace_html"] = ""
1626
+ thr["append"].clear()
1627
+ thr["code"] = False
1628
+
1629
+ def _throttle_queue(
1630
+ self,
1631
+ pid: int,
1632
+ name: str,
1633
+ html: str,
1634
+ text_chunk: str,
1635
+ replace: bool,
1636
+ is_code_block: bool
1637
+ ):
1638
+ """
1639
+ Queue chunk for throttled emit
1640
+
1641
+ :param pid: context PID
1642
+ :param name: name of the chunk
1643
+ :param html: HTML content of the chunk
1644
+ :param text_chunk: raw text chunk
1645
+ :param replace: whether to replace the current content
1646
+ :param is_code_block: whether the chunk is a code block
1647
+ """
1648
+ thr = self._throttle_get(pid)
1649
+ if name:
1650
+ thr["name"] = name
1651
+ if replace:
1652
+ thr["op"] = 1
1653
+ thr["replace_html"] = html
1654
+ thr["append"].clear()
1655
+ thr["code"] = bool(is_code_block)
1656
+ else:
1657
+ if thr["op"] != 1:
1658
+ thr["op"] = 2
1659
+ thr["append"].append(text_chunk)
1660
+ thr["code"] = bool(is_code_block)
1661
+
1662
+ def _throttle_emit(self, pid: int, force: bool = False):
1663
+ """
1664
+ Emit queued chunks if due
1665
+
1666
+ :param pid: context PID
1667
+ :param force: force emit regardless of throttle interval
1668
+ """
1669
+ thr = self._throttle_get(pid)
1670
+ now = monotonic()
1671
+ if not force and (now - thr["last"] < self._throttle_interval):
1672
+ return
1673
+ if thr["op"] == 1:
1674
+ try:
1675
+ node = self.get_output_node_by_pid(pid)
1676
+ if node is not None:
1677
+ node.page().bridge.chunk.emit(
1678
+ thr["name"],
1679
+ self.sanitize_html(thr["replace_html"]),
1680
+ "",
1681
+ True,
1682
+ bool(thr["code"]),
1683
+ )
1684
+ except Exception:
1685
+ pass
1686
+ thr["last"] = now
1687
+ self._throttle_reset(pid)
1688
+ elif thr["op"] == 2 and thr["append"]:
1689
+ append_str = "".join(thr["append"])
1690
+ try:
1691
+ node = self.get_output_node_by_pid(pid)
1692
+ if node is not None:
1693
+ node.page().bridge.chunk.emit(
1694
+ thr["name"],
1695
+ "",
1696
+ self.sanitize_html(append_str),
1697
+ False,
1698
+ bool(thr["code"]),
1699
+ )
1700
+ except Exception:
1701
+ pass
1702
+ thr["last"] = now
1703
+ self._throttle_reset(pid)
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "__meta__": {
3
- "version": "2.6.10",
4
- "app.version": "2.6.10",
5
- "updated_at": "2025-08-17T00:00:00"
3
+ "version": "2.6.12",
4
+ "app.version": "2.6.12",
5
+ "updated_at": "2025-08-19T00:00:00"
6
6
  },
7
7
  "access.audio.event.speech": false,
8
8
  "access.audio.event.speech.disabled": [],
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "__meta__": {
3
- "version": "2.6.10",
4
- "app.version": "2.6.10",
5
- "updated_at": "2025-08-17T23:07:35"
3
+ "version": "2.6.12",
4
+ "app.version": "2.6.12",
5
+ "updated_at": "2025-08-19T23:07:35"
6
6
  },
7
7
  "items": {
8
8
  "SpeakLeash/bielik-11b-v2.3-instruct:Q4_K_M": {
@@ -21,7 +21,7 @@
21
21
  "function": []
22
22
  },
23
23
  "experts": [],
24
- "idx": "base",
24
+ "idx": "_",
25
25
  "agent_provider": "openai",
26
26
  "assistant_id": "",
27
27
  "enabled": true,
@@ -21,7 +21,7 @@
21
21
  "function": []
22
22
  },
23
23
  "experts": [],
24
- "idx": "base",
24
+ "idx": "_",
25
25
  "agent_provider": "openai_assistant",
26
26
  "assistant_id": "",
27
27
  "enabled": true,
@@ -21,7 +21,7 @@
21
21
  "function": []
22
22
  },
23
23
  "experts": [],
24
- "idx": "base",
24
+ "idx": "_",
25
25
  "agent_provider": "planner",
26
26
  "assistant_id": "",
27
27
  "enabled": true,
@@ -21,7 +21,7 @@
21
21
  "function": []
22
22
  },
23
23
  "experts": [],
24
- "idx": "base",
24
+ "idx": "_",
25
25
  "agent_provider": "react",
26
26
  "assistant_id": "",
27
27
  "enabled": true,