pygpt-net 2.6.43__py3-none-any.whl → 2.6.45__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.
- pygpt_net/CHANGELOG.txt +13 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +1 -1
- pygpt_net/controller/ctx/ctx.py +6 -0
- pygpt_net/controller/debug/debug.py +7 -19
- pygpt_net/controller/debug/fixtures.py +103 -0
- pygpt_net/core/debug/console/console.py +2 -1
- pygpt_net/core/debug/debug.py +1 -1
- pygpt_net/core/fixtures/stream/__init__.py +0 -0
- pygpt_net/{provider/api/fake → core/fixtures/stream}/generator.py +2 -3
- pygpt_net/core/render/web/body.py +294 -23
- pygpt_net/core/render/web/helpers.py +26 -0
- pygpt_net/core/render/web/renderer.py +457 -704
- pygpt_net/data/config/config.json +10 -6
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/settings.json +59 -19
- pygpt_net/data/fixtures/fake_stream.txt +5733 -0
- pygpt_net/data/js/app.js +2617 -1315
- pygpt_net/data/locale/locale.en.ini +12 -5
- pygpt_net/js_rc.py +14272 -10602
- pygpt_net/provider/api/openai/__init__.py +4 -12
- pygpt_net/provider/core/config/patch.py +14 -1
- pygpt_net/ui/base/context_menu.py +3 -2
- pygpt_net/ui/layout/chat/output.py +1 -1
- pygpt_net/ui/layout/ctx/ctx_list.py +3 -3
- pygpt_net/ui/menu/debug.py +36 -23
- pygpt_net/ui/widget/lists/context.py +233 -51
- pygpt_net/ui/widget/textarea/web.py +4 -4
- pygpt_net/utils.py +3 -2
- {pygpt_net-2.6.43.dist-info → pygpt_net-2.6.45.dist-info}/METADATA +72 -14
- {pygpt_net-2.6.43.dist-info → pygpt_net-2.6.45.dist-info}/RECORD +35 -32
- /pygpt_net/{provider/api/fake/__init__.py → core/fixtures/__init__} +0 -0
- {pygpt_net-2.6.43.dist-info → pygpt_net-2.6.45.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.43.dist-info → pygpt_net-2.6.45.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.43.dist-info → pygpt_net-2.6.45.dist-info}/entry_points.txt +0 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2025.09.
|
|
9
|
+
# Updated Date: 2025.09.13 06:10:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import gc
|
|
@@ -17,7 +17,7 @@ import html as _html
|
|
|
17
17
|
from dataclasses import dataclass, field
|
|
18
18
|
|
|
19
19
|
from datetime import datetime
|
|
20
|
-
from typing import Optional, List, Any
|
|
20
|
+
from typing import Optional, List, Any, Dict, Tuple
|
|
21
21
|
from time import monotonic
|
|
22
22
|
from io import StringIO
|
|
23
23
|
|
|
@@ -41,6 +41,55 @@ from .pid import PidData
|
|
|
41
41
|
from pygpt_net.core.events import RenderEvent
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
@dataclass(slots=True)
|
|
45
|
+
class RenderBlock:
|
|
46
|
+
"""
|
|
47
|
+
JSON payload for node rendering in JS templates.
|
|
48
|
+
|
|
49
|
+
Keep only raw data here. HTML is avoided except where there is
|
|
50
|
+
no easy way to keep raw (e.g. plugin-provided tool extras), which are
|
|
51
|
+
carried under extra.tool_extra_html.
|
|
52
|
+
"""
|
|
53
|
+
id: int
|
|
54
|
+
meta_id: Optional[int] = None
|
|
55
|
+
input: Optional[dict] = None
|
|
56
|
+
output: Optional[dict] = None
|
|
57
|
+
files: dict = field(default_factory=dict)
|
|
58
|
+
images: dict = field(default_factory=dict)
|
|
59
|
+
urls: dict = field(default_factory=dict)
|
|
60
|
+
tools: dict = field(default_factory=dict)
|
|
61
|
+
tools_outputs: dict = field(default_factory=dict)
|
|
62
|
+
extra: dict = field(default_factory=dict)
|
|
63
|
+
|
|
64
|
+
def to_dict(self) -> dict:
|
|
65
|
+
return {
|
|
66
|
+
"id": self.id,
|
|
67
|
+
"meta_id": self.meta_id,
|
|
68
|
+
"input": self.input,
|
|
69
|
+
"output": self.output,
|
|
70
|
+
"files": self.files,
|
|
71
|
+
"images": self.images,
|
|
72
|
+
"urls": self.urls,
|
|
73
|
+
"tools": self.tools,
|
|
74
|
+
"tools_outputs": self.tools_outputs,
|
|
75
|
+
"extra": self.extra,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
def to_json(self, wrap: bool = True) -> str:
|
|
79
|
+
"""
|
|
80
|
+
Convert node to JSON string.
|
|
81
|
+
|
|
82
|
+
:param wrap: wrap into {"node": {...}} (single appendNode case)
|
|
83
|
+
"""
|
|
84
|
+
data = self.to_dict()
|
|
85
|
+
if wrap:
|
|
86
|
+
return json.dumps({"node": data}, ensure_ascii=False, separators=(',', ':'))
|
|
87
|
+
return json.dumps(data, ensure_ascii=False, separators=(',', ':'))
|
|
88
|
+
|
|
89
|
+
def debug(self) -> str:
|
|
90
|
+
return json.dumps(self.to_dict(), ensure_ascii=False, indent=2)
|
|
91
|
+
|
|
92
|
+
|
|
44
93
|
class Renderer(BaseRenderer):
|
|
45
94
|
|
|
46
95
|
NODE_INPUT = 0
|
|
@@ -71,7 +120,6 @@ class Renderer(BaseRenderer):
|
|
|
71
120
|
return ""
|
|
72
121
|
data = self._buf.getvalue()
|
|
73
122
|
old = self._buf
|
|
74
|
-
# Replace the internal buffer instance to drop capacity immediately
|
|
75
123
|
self._buf = StringIO()
|
|
76
124
|
self._size = 0
|
|
77
125
|
try:
|
|
@@ -124,13 +172,10 @@ class Renderer(BaseRenderer):
|
|
|
124
172
|
# ------------------------------------------------------------------
|
|
125
173
|
# Python-side micro-batching for streaming
|
|
126
174
|
# ------------------------------------------------------------------
|
|
127
|
-
# render.stream.interval_ms (int, default 100)
|
|
128
|
-
# render.stream.max_bytes (int, default 8192)
|
|
129
|
-
# render.stream.emergency_bytes (int, default 524288)
|
|
130
175
|
|
|
131
|
-
self._stream_interval_ms: int = _cfg('render.stream.interval_ms', 30)
|
|
132
|
-
self._stream_max_bytes: int = _cfg('render.stream.max_bytes', 8 * 1024)
|
|
133
|
-
self._stream_emergency_bytes: int = _cfg('render.stream.emergency_bytes', 512 * 1024)
|
|
176
|
+
self._stream_interval_ms: int = _cfg('render.stream.interval_ms', 30)
|
|
177
|
+
self._stream_max_bytes: int = _cfg('render.stream.max_bytes', 8 * 1024)
|
|
178
|
+
self._stream_emergency_bytes: int = _cfg('render.stream.emergency_bytes', 512 * 1024)
|
|
134
179
|
|
|
135
180
|
# Per-PID streaming state
|
|
136
181
|
self._stream_acc: dict[int, Renderer._AppendBuffer] = {}
|
|
@@ -280,7 +325,7 @@ class Renderer(BaseRenderer):
|
|
|
280
325
|
"""
|
|
281
326
|
On kernel state changed event
|
|
282
327
|
|
|
283
|
-
:param state: state
|
|
328
|
+
:param state: new state
|
|
284
329
|
:param meta: context meta
|
|
285
330
|
"""
|
|
286
331
|
if state == RenderEvent.STATE_BUSY:
|
|
@@ -292,7 +337,7 @@ class Renderer(BaseRenderer):
|
|
|
292
337
|
node.page().runJavaScript(
|
|
293
338
|
"if (typeof window.showLoading !== 'undefined') showLoading();"
|
|
294
339
|
)
|
|
295
|
-
except Exception
|
|
340
|
+
except Exception:
|
|
296
341
|
pass
|
|
297
342
|
|
|
298
343
|
elif state == RenderEvent.STATE_IDLE:
|
|
@@ -303,7 +348,7 @@ class Renderer(BaseRenderer):
|
|
|
303
348
|
node.page().runJavaScript(
|
|
304
349
|
"if (typeof window.hideLoading !== 'undefined') hideLoading();"
|
|
305
350
|
)
|
|
306
|
-
except Exception
|
|
351
|
+
except Exception:
|
|
307
352
|
pass
|
|
308
353
|
|
|
309
354
|
elif state == RenderEvent.STATE_ERROR:
|
|
@@ -314,21 +359,16 @@ class Renderer(BaseRenderer):
|
|
|
314
359
|
node.page().runJavaScript(
|
|
315
360
|
"if (typeof window.hideLoading !== 'undefined') hideLoading();"
|
|
316
361
|
)
|
|
317
|
-
except Exception
|
|
362
|
+
except Exception:
|
|
318
363
|
pass
|
|
319
364
|
|
|
320
|
-
def begin(
|
|
321
|
-
self,
|
|
322
|
-
meta: CtxMeta,
|
|
323
|
-
ctx: CtxItem,
|
|
324
|
-
stream: bool = False
|
|
325
|
-
):
|
|
365
|
+
def begin(self, meta: CtxMeta, ctx: CtxItem, stream: bool = False):
|
|
326
366
|
"""
|
|
327
367
|
Render begin
|
|
328
368
|
|
|
329
369
|
:param meta: context meta
|
|
330
370
|
:param ctx: context item
|
|
331
|
-
:param stream: True if
|
|
371
|
+
:param stream: True if streaming mode
|
|
332
372
|
"""
|
|
333
373
|
pid = self.get_or_create_pid(meta)
|
|
334
374
|
self.init(pid)
|
|
@@ -336,18 +376,13 @@ class Renderer(BaseRenderer):
|
|
|
336
376
|
self.tool_output_end()
|
|
337
377
|
self.prev_chunk_replace = False
|
|
338
378
|
|
|
339
|
-
def end(
|
|
340
|
-
self,
|
|
341
|
-
meta: CtxMeta,
|
|
342
|
-
ctx: CtxItem,
|
|
343
|
-
stream: bool = False
|
|
344
|
-
):
|
|
379
|
+
def end(self, meta: CtxMeta, ctx: CtxItem, stream: bool = False):
|
|
345
380
|
"""
|
|
346
381
|
Render end
|
|
347
382
|
|
|
348
383
|
:param meta: context meta
|
|
349
384
|
:param ctx: context item
|
|
350
|
-
:param stream: True if
|
|
385
|
+
:param stream: True if streaming mode
|
|
351
386
|
"""
|
|
352
387
|
pid = self.get_or_create_pid(meta)
|
|
353
388
|
if pid is None:
|
|
@@ -358,30 +393,19 @@ class Renderer(BaseRenderer):
|
|
|
358
393
|
else:
|
|
359
394
|
self.reload()
|
|
360
395
|
self.pids[pid].clear()
|
|
361
|
-
|
|
362
|
-
# memory cleanup if needed
|
|
363
396
|
self.auto_cleanup(meta)
|
|
364
397
|
|
|
365
|
-
def end_extra(
|
|
366
|
-
self,
|
|
367
|
-
meta: CtxMeta,
|
|
368
|
-
ctx: CtxItem,
|
|
369
|
-
stream: bool = False
|
|
370
|
-
):
|
|
398
|
+
def end_extra(self, meta: CtxMeta, ctx: CtxItem, stream: bool = False):
|
|
371
399
|
"""
|
|
372
400
|
Render end extra
|
|
373
401
|
|
|
374
402
|
:param meta: context meta
|
|
375
403
|
:param ctx: context item
|
|
376
|
-
:param stream: True if
|
|
404
|
+
:param stream: True if streaming mode
|
|
377
405
|
"""
|
|
378
406
|
self.to_end(ctx)
|
|
379
407
|
|
|
380
|
-
def stream_begin(
|
|
381
|
-
self,
|
|
382
|
-
meta: CtxMeta,
|
|
383
|
-
ctx: CtxItem
|
|
384
|
-
):
|
|
408
|
+
def stream_begin(self, meta: CtxMeta, ctx: CtxItem):
|
|
385
409
|
"""
|
|
386
410
|
Render stream begin
|
|
387
411
|
|
|
@@ -401,18 +425,13 @@ class Renderer(BaseRenderer):
|
|
|
401
425
|
except Exception:
|
|
402
426
|
pass
|
|
403
427
|
|
|
404
|
-
# cache name header once per stream (used by JS to show avatar/name)
|
|
405
428
|
try:
|
|
406
429
|
self.pids[pid].header = self.get_name_header(ctx, stream=True)
|
|
407
430
|
except Exception:
|
|
408
431
|
self.pids[pid].header = ""
|
|
409
432
|
self.update_names(meta, ctx)
|
|
410
433
|
|
|
411
|
-
def stream_end(
|
|
412
|
-
self,
|
|
413
|
-
meta: CtxMeta,
|
|
414
|
-
ctx: CtxItem
|
|
415
|
-
):
|
|
434
|
+
def stream_end(self, meta: CtxMeta, ctx: CtxItem):
|
|
416
435
|
"""
|
|
417
436
|
Render stream end
|
|
418
437
|
|
|
@@ -424,7 +443,6 @@ class Renderer(BaseRenderer):
|
|
|
424
443
|
if pid is None:
|
|
425
444
|
return
|
|
426
445
|
|
|
427
|
-
# Flush any pending micro-batch before we close the stream
|
|
428
446
|
self._stream_flush(pid, force=True)
|
|
429
447
|
if self.window.controller.agent.legacy.enabled():
|
|
430
448
|
if self.pids[pid].item is not None:
|
|
@@ -435,24 +453,21 @@ class Renderer(BaseRenderer):
|
|
|
435
453
|
self.get_output_node(meta).page().runJavaScript("if (typeof window.endStream !== 'undefined') endStream();")
|
|
436
454
|
except Exception:
|
|
437
455
|
pass
|
|
438
|
-
self._stream_reset(pid)
|
|
439
|
-
|
|
440
|
-
# memory cleanup if needed
|
|
456
|
+
self._stream_reset(pid)
|
|
441
457
|
self.auto_cleanup(meta)
|
|
442
458
|
|
|
443
459
|
def auto_cleanup(self, meta: CtxMeta):
|
|
444
460
|
"""
|
|
445
461
|
Automatic cleanup after context is done
|
|
446
462
|
|
|
447
|
-
|
|
463
|
+
If memory limit is set, perform fresh() cleanup when exceeded.
|
|
448
464
|
"""
|
|
449
|
-
# if memory limit reached - destroy old page view
|
|
450
465
|
try:
|
|
451
466
|
limit_bytes = parse_bytes(self.window.core.config.get('render.memory.limit', 0))
|
|
452
467
|
except Exception as e:
|
|
453
468
|
self.window.core.debug.log("[Renderer] auto-cleanup:", e)
|
|
454
469
|
limit_bytes = 0
|
|
455
|
-
|
|
470
|
+
|
|
456
471
|
if limit_bytes <= 0:
|
|
457
472
|
self.auto_cleanup_soft(meta)
|
|
458
473
|
return
|
|
@@ -474,47 +489,36 @@ class Renderer(BaseRenderer):
|
|
|
474
489
|
|
|
475
490
|
def auto_cleanup_soft(self, meta: CtxMeta = None):
|
|
476
491
|
"""
|
|
477
|
-
|
|
492
|
+
Try to trim memory on Linux
|
|
478
493
|
|
|
479
|
-
|
|
494
|
+
Soft cleanup, called after each context is done.
|
|
480
495
|
"""
|
|
481
496
|
try:
|
|
482
497
|
malloc_trim_linux()
|
|
483
498
|
except Exception:
|
|
484
499
|
pass
|
|
485
500
|
|
|
486
|
-
def append_context(
|
|
487
|
-
self,
|
|
488
|
-
meta: CtxMeta,
|
|
489
|
-
items: List[CtxItem],
|
|
490
|
-
clear: bool = True
|
|
491
|
-
):
|
|
501
|
+
def append_context(self, meta: CtxMeta, items: List[CtxItem], clear: bool = True):
|
|
492
502
|
"""
|
|
493
503
|
Append all context items to output
|
|
494
504
|
|
|
495
|
-
:param meta:
|
|
496
|
-
:param items: context items
|
|
497
|
-
:param clear:
|
|
505
|
+
:param meta: context meta
|
|
506
|
+
:param items: list of context items
|
|
507
|
+
:param clear: clear previous content
|
|
498
508
|
"""
|
|
499
509
|
self.tool_output_end()
|
|
500
|
-
self.append_context_all(
|
|
501
|
-
meta,
|
|
502
|
-
items,
|
|
503
|
-
clear=clear,
|
|
504
|
-
)
|
|
510
|
+
self.append_context_all(meta, items, clear=clear)
|
|
505
511
|
|
|
506
|
-
def append_context_partial(
|
|
507
|
-
self,
|
|
508
|
-
meta: CtxMeta,
|
|
509
|
-
items: List[CtxItem],
|
|
510
|
-
clear: bool = True
|
|
511
|
-
):
|
|
512
|
+
def append_context_partial(self, meta: CtxMeta, items: List[CtxItem], clear: bool = True):
|
|
512
513
|
"""
|
|
513
|
-
Append
|
|
514
|
+
Append context items part-by-part
|
|
514
515
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
516
|
+
Append each item as it comes, useful for non-streaming mode when
|
|
517
|
+
context is built gradually.
|
|
518
|
+
|
|
519
|
+
:param meta: context meta
|
|
520
|
+
:param items: list of context items
|
|
521
|
+
:param clear: clear previous content
|
|
518
522
|
"""
|
|
519
523
|
if len(items) == 0:
|
|
520
524
|
if meta is None:
|
|
@@ -522,7 +526,6 @@ class Renderer(BaseRenderer):
|
|
|
522
526
|
|
|
523
527
|
pid = self.get_or_create_pid(meta)
|
|
524
528
|
self.init(pid)
|
|
525
|
-
|
|
526
529
|
if clear:
|
|
527
530
|
self.reset(meta)
|
|
528
531
|
|
|
@@ -537,36 +540,32 @@ class Renderer(BaseRenderer):
|
|
|
537
540
|
if i == 0:
|
|
538
541
|
item.first = True
|
|
539
542
|
next_item = items[i + 1] if i + 1 < total else None
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
prev_ctx=
|
|
544
|
-
|
|
545
|
-
|
|
543
|
+
|
|
544
|
+
# ignore hidden
|
|
545
|
+
if item.hidden:
|
|
546
|
+
prev_ctx = item
|
|
547
|
+
continue
|
|
548
|
+
|
|
549
|
+
# build single RenderBlock with both input and output (if present)
|
|
550
|
+
input_text = self.prepare_input(meta, item, flush=False, append=False)
|
|
551
|
+
output_text = self.prepare_output(meta, item, flush=False, prev_ctx=prev_ctx, next_ctx=next_item)
|
|
552
|
+
block = self._build_render_block(meta, item, input_text, output_text, prev_ctx=prev_ctx, next_ctx=next_item)
|
|
553
|
+
if block:
|
|
554
|
+
self.append(pid, block.to_json(wrap=True))
|
|
555
|
+
|
|
546
556
|
prev_ctx = item
|
|
547
557
|
|
|
548
|
-
prev_ctx = None
|
|
549
|
-
next_item = None
|
|
550
558
|
self.pids[pid].use_buffer = False
|
|
551
559
|
if self.pids[pid].html != "":
|
|
552
|
-
self.append(
|
|
553
|
-
pid,
|
|
554
|
-
self.pids[pid].html,
|
|
555
|
-
flush=True,
|
|
556
|
-
)
|
|
560
|
+
self.append(pid, self.pids[pid].html, flush=True)
|
|
557
561
|
|
|
558
|
-
def append_context_all(
|
|
559
|
-
self,
|
|
560
|
-
meta: CtxMeta,
|
|
561
|
-
items: List[CtxItem],
|
|
562
|
-
clear: bool = True
|
|
563
|
-
):
|
|
562
|
+
def append_context_all(self, meta: CtxMeta, items: List[CtxItem], clear: bool = True):
|
|
564
563
|
"""
|
|
565
|
-
Append
|
|
564
|
+
Append whole context at once, using JSON nodes
|
|
566
565
|
|
|
567
|
-
:param meta:
|
|
568
|
-
:param items: context items
|
|
569
|
-
:param clear:
|
|
566
|
+
:param meta: context meta
|
|
567
|
+
:param items: list of context items
|
|
568
|
+
:param clear: clear previous content
|
|
570
569
|
"""
|
|
571
570
|
if len(items) == 0:
|
|
572
571
|
if meta is None:
|
|
@@ -576,14 +575,16 @@ class Renderer(BaseRenderer):
|
|
|
576
575
|
self.init(pid)
|
|
577
576
|
|
|
578
577
|
if clear:
|
|
579
|
-
|
|
578
|
+
# nodes will be cleared on JS when replace flag is True
|
|
579
|
+
self.reset(meta, clear_nodes=False)
|
|
580
580
|
|
|
581
581
|
self.pids[pid].use_buffer = True
|
|
582
582
|
self.pids[pid].html = ""
|
|
583
583
|
prev_ctx = None
|
|
584
584
|
next_ctx = None
|
|
585
585
|
total = len(items)
|
|
586
|
-
|
|
586
|
+
nodes: List[dict] = []
|
|
587
|
+
|
|
587
588
|
for i, item in enumerate(items):
|
|
588
589
|
self.update_names(meta, item)
|
|
589
590
|
item.idx = i
|
|
@@ -591,82 +592,37 @@ class Renderer(BaseRenderer):
|
|
|
591
592
|
item.first = True
|
|
592
593
|
next_ctx = items[i + 1] if i + 1 < total else None
|
|
593
594
|
|
|
594
|
-
# ignore hidden items
|
|
595
595
|
if item.hidden:
|
|
596
596
|
prev_ctx = item
|
|
597
597
|
continue
|
|
598
598
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
ctx=item,
|
|
605
|
-
html=data,
|
|
606
|
-
type=self.NODE_INPUT,
|
|
607
|
-
prev_ctx=prev_ctx,
|
|
608
|
-
next_ctx=next_ctx,
|
|
609
|
-
)
|
|
610
|
-
if html:
|
|
611
|
-
html_parts.append(html)
|
|
612
|
-
|
|
613
|
-
# output node
|
|
614
|
-
data = self.prepare_output(
|
|
615
|
-
meta,
|
|
616
|
-
item,
|
|
617
|
-
flush=False,
|
|
618
|
-
prev_ctx=prev_ctx,
|
|
619
|
-
next_ctx=next_ctx,
|
|
620
|
-
)
|
|
621
|
-
if data:
|
|
622
|
-
html = self.prepare_node(
|
|
623
|
-
meta=meta,
|
|
624
|
-
ctx=item,
|
|
625
|
-
html=data,
|
|
626
|
-
type=self.NODE_OUTPUT,
|
|
627
|
-
prev_ctx=prev_ctx,
|
|
628
|
-
next_ctx=next_ctx,
|
|
629
|
-
)
|
|
630
|
-
if html:
|
|
631
|
-
html_parts.append(html)
|
|
599
|
+
input_text = self.prepare_input(meta, item, flush=False, append=False)
|
|
600
|
+
output_text = self.prepare_output(meta, item, flush=False, prev_ctx=prev_ctx, next_ctx=next_ctx)
|
|
601
|
+
block = self._build_render_block(meta, item, input_text, output_text, prev_ctx=prev_ctx, next_ctx=next_ctx)
|
|
602
|
+
if block:
|
|
603
|
+
nodes.append(block.to_dict())
|
|
632
604
|
|
|
633
605
|
prev_ctx = item
|
|
634
606
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
self.append(
|
|
638
|
-
pid,
|
|
639
|
-
"".join(html_parts),
|
|
640
|
-
replace=True,
|
|
641
|
-
)
|
|
607
|
+
if nodes:
|
|
608
|
+
payload = json.dumps({"nodes": nodes}, ensure_ascii=False, separators=(',', ':'))
|
|
609
|
+
self.append(pid, payload, replace=True)
|
|
642
610
|
|
|
643
|
-
html_parts.clear()
|
|
644
|
-
html_parts = None
|
|
645
611
|
prev_ctx = None
|
|
646
612
|
next_ctx = None
|
|
647
613
|
self.pids[pid].use_buffer = False
|
|
648
614
|
if self.pids[pid].html != "":
|
|
649
|
-
self.append(
|
|
650
|
-
pid,
|
|
651
|
-
self.pids[pid].html,
|
|
652
|
-
flush=True,
|
|
653
|
-
replace=True,
|
|
654
|
-
)
|
|
615
|
+
self.append(pid, self.pids[pid].html, flush=True, replace=True)
|
|
655
616
|
|
|
656
|
-
def prepare_input(
|
|
657
|
-
self, meta: CtxMeta,
|
|
658
|
-
ctx: CtxItem,
|
|
659
|
-
flush: bool = True,
|
|
660
|
-
append: bool = False
|
|
661
|
-
) -> Optional[str]:
|
|
617
|
+
def prepare_input(self, meta: CtxMeta, ctx: CtxItem, flush: bool = True, append: bool = False) -> Optional[str]:
|
|
662
618
|
"""
|
|
663
|
-
Prepare text input
|
|
619
|
+
Prepare text input (raw, no HTML)
|
|
664
620
|
|
|
665
621
|
:param meta: context meta
|
|
666
622
|
:param ctx: context item
|
|
667
|
-
:param flush: flush
|
|
668
|
-
:param append: True if
|
|
669
|
-
:return:
|
|
623
|
+
:param flush: True if flush input area (legacy)
|
|
624
|
+
:param append: True if append to input area (legacy)
|
|
625
|
+
:return: prepared text or None
|
|
670
626
|
"""
|
|
671
627
|
if ctx.input is None or ctx.input == "":
|
|
672
628
|
return
|
|
@@ -691,21 +647,16 @@ class Renderer(BaseRenderer):
|
|
|
691
647
|
if ctx.internal and ctx.input.startswith("user: "):
|
|
692
648
|
text = re.sub(r'^user: ', '> ', ctx.input)
|
|
693
649
|
|
|
694
|
-
return text.strip()
|
|
650
|
+
return str(text).strip()
|
|
695
651
|
|
|
696
|
-
def append_input(
|
|
697
|
-
self, meta: CtxMeta,
|
|
698
|
-
ctx: CtxItem,
|
|
699
|
-
flush: bool = True,
|
|
700
|
-
append: bool = False
|
|
701
|
-
):
|
|
652
|
+
def append_input(self, meta: CtxMeta, ctx: CtxItem, flush: bool = True, append: bool = False):
|
|
702
653
|
"""
|
|
703
|
-
Append
|
|
654
|
+
Append user input as RenderBlock JSON
|
|
704
655
|
|
|
705
656
|
:param meta: context meta
|
|
706
657
|
:param ctx: context item
|
|
707
|
-
:param flush: flush
|
|
708
|
-
:param append: True if
|
|
658
|
+
:param flush: True if flush input area (legacy)
|
|
659
|
+
:param append: True if append to input area (legacy)
|
|
709
660
|
"""
|
|
710
661
|
self.tool_output_end()
|
|
711
662
|
pid = self.get_or_create_pid(meta)
|
|
@@ -717,35 +668,25 @@ class Renderer(BaseRenderer):
|
|
|
717
668
|
if text:
|
|
718
669
|
if flush:
|
|
719
670
|
if self.is_stream() and not append:
|
|
671
|
+
# legacy streaming input (leave as-is)
|
|
720
672
|
content = self.prepare_node(meta, ctx, text, self.NODE_INPUT)
|
|
721
673
|
self.append_chunk_input(meta, ctx, content, begin=False)
|
|
722
|
-
text = None
|
|
723
674
|
return
|
|
724
|
-
self.
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
html=text,
|
|
728
|
-
type=self.NODE_INPUT,
|
|
729
|
-
)
|
|
730
|
-
text = None # free reference
|
|
675
|
+
block = self._build_render_block(meta, ctx, input_text=text, output_text=None)
|
|
676
|
+
if block:
|
|
677
|
+
self.append(pid, block.to_json(wrap=True))
|
|
731
678
|
|
|
732
|
-
def prepare_output(
|
|
733
|
-
|
|
734
|
-
meta: CtxMeta,
|
|
735
|
-
ctx: CtxItem,
|
|
736
|
-
flush: bool = True,
|
|
737
|
-
prev_ctx: Optional[CtxItem] = None,
|
|
738
|
-
next_ctx: Optional[CtxItem] = None
|
|
739
|
-
) -> Optional[str]:
|
|
679
|
+
def prepare_output(self, meta: CtxMeta, ctx: CtxItem, flush: bool = True,
|
|
680
|
+
prev_ctx: Optional[CtxItem] = None, next_ctx: Optional[CtxItem] = None) -> Optional[str]:
|
|
740
681
|
"""
|
|
741
|
-
Prepare text output
|
|
682
|
+
Prepare text output (raw markdown text)
|
|
742
683
|
|
|
743
684
|
:param meta: context meta
|
|
744
685
|
:param ctx: context item
|
|
745
|
-
:param flush: flush
|
|
746
|
-
:param prev_ctx: previous context
|
|
747
|
-
:param next_ctx: next context
|
|
748
|
-
:return:
|
|
686
|
+
:param flush: True if flush output area (legacy)
|
|
687
|
+
:param prev_ctx: previous context item
|
|
688
|
+
:param next_ctx: next context item
|
|
689
|
+
:return: prepared text or None
|
|
749
690
|
"""
|
|
750
691
|
output = ctx.output
|
|
751
692
|
if isinstance(ctx.extra, dict) and ctx.extra.get("output"):
|
|
@@ -756,60 +697,36 @@ class Renderer(BaseRenderer):
|
|
|
756
697
|
else:
|
|
757
698
|
if not output:
|
|
758
699
|
return
|
|
759
|
-
return output.strip()
|
|
700
|
+
return str(output).strip()
|
|
760
701
|
|
|
761
|
-
def append_output(
|
|
762
|
-
|
|
763
|
-
meta: CtxMeta,
|
|
764
|
-
ctx: CtxItem,
|
|
765
|
-
flush: bool = True,
|
|
766
|
-
prev_ctx: Optional[CtxItem] = None,
|
|
767
|
-
next_ctx: Optional[CtxItem] = None
|
|
768
|
-
):
|
|
702
|
+
def append_output(self, meta: CtxMeta, ctx: CtxItem, flush: bool = True,
|
|
703
|
+
prev_ctx: Optional[CtxItem] = None, next_ctx: Optional[CtxItem] = None):
|
|
769
704
|
"""
|
|
770
|
-
Append
|
|
705
|
+
Append bot output as RenderBlock JSON
|
|
771
706
|
|
|
772
707
|
:param meta: context meta
|
|
773
708
|
:param ctx: context item
|
|
774
|
-
:param flush: flush
|
|
775
|
-
:param prev_ctx: previous context
|
|
776
|
-
:param next_ctx: next context
|
|
709
|
+
:param flush: True if flush output area (legacy)
|
|
710
|
+
:param prev_ctx: previous context item
|
|
711
|
+
:param next_ctx: next context item
|
|
777
712
|
"""
|
|
778
713
|
self.tool_output_end()
|
|
779
|
-
output = self.prepare_output(
|
|
780
|
-
meta=meta,
|
|
781
|
-
ctx=ctx,
|
|
782
|
-
flush=flush,
|
|
783
|
-
prev_ctx=prev_ctx,
|
|
784
|
-
next_ctx=next_ctx,
|
|
785
|
-
)
|
|
714
|
+
output = self.prepare_output(meta=meta, ctx=ctx, flush=flush, prev_ctx=prev_ctx, next_ctx=next_ctx)
|
|
786
715
|
if output:
|
|
787
|
-
self.
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
prev_ctx=prev_ctx,
|
|
793
|
-
next_ctx=next_ctx,
|
|
794
|
-
)
|
|
716
|
+
block = self._build_render_block(meta, ctx, input_text=None, output_text=output,
|
|
717
|
+
prev_ctx=prev_ctx, next_ctx=next_ctx)
|
|
718
|
+
if block:
|
|
719
|
+
pid = self.get_or_create_pid(meta)
|
|
720
|
+
self.append(pid, block.to_json(wrap=True))
|
|
795
721
|
|
|
796
|
-
def append_chunk(
|
|
797
|
-
self,
|
|
798
|
-
meta: CtxMeta,
|
|
799
|
-
ctx: CtxItem,
|
|
800
|
-
text_chunk: str,
|
|
801
|
-
begin: bool = False
|
|
802
|
-
):
|
|
722
|
+
def append_chunk(self, meta: CtxMeta, ctx: CtxItem, text_chunk: str, begin: bool = False):
|
|
803
723
|
"""
|
|
804
|
-
Append streamed Markdown chunk to JS with micro-batching.
|
|
805
|
-
- No Python-side parsing
|
|
806
|
-
- No Python-side full buffer retention
|
|
807
|
-
- Minimal signal rate thanks to micro-batching (per PID)
|
|
724
|
+
Append streamed Markdown chunk to JS with micro-batching and typed chunk support.
|
|
808
725
|
|
|
809
726
|
:param meta: context meta
|
|
810
727
|
:param ctx: context item
|
|
811
|
-
:param text_chunk: text chunk
|
|
812
|
-
:param begin: if
|
|
728
|
+
:param text_chunk: text chunk to append
|
|
729
|
+
:param begin: True if begin of stream
|
|
813
730
|
"""
|
|
814
731
|
pid = self.get_or_create_pid(meta)
|
|
815
732
|
if pid is None:
|
|
@@ -819,14 +736,12 @@ class Renderer(BaseRenderer):
|
|
|
819
736
|
pctx.item = ctx
|
|
820
737
|
|
|
821
738
|
if begin:
|
|
822
|
-
# Reset stream area and loader on the JS side
|
|
823
739
|
try:
|
|
824
740
|
self.get_output_node(meta).page().runJavaScript(
|
|
825
741
|
"if (typeof window.beginStream !== 'undefined') beginStream(true);"
|
|
826
742
|
)
|
|
827
743
|
except Exception:
|
|
828
744
|
pass
|
|
829
|
-
# Prepare name header for this stream (avatar/name)
|
|
830
745
|
pctx.header = self.get_name_header(ctx, stream=True)
|
|
831
746
|
self._stream_reset(pid)
|
|
832
747
|
self.update_names(meta, ctx)
|
|
@@ -834,16 +749,9 @@ class Renderer(BaseRenderer):
|
|
|
834
749
|
if not text_chunk:
|
|
835
750
|
return
|
|
836
751
|
|
|
837
|
-
# Push chunk into per-PID micro-batch buffer (will be flushed by a QTimer)
|
|
838
752
|
self._stream_push(pid, pctx.header or "", str(text_chunk))
|
|
839
|
-
# self.get_output_node(meta).page().bridge.chunk.emit(pctx.header or "", str(text_chunk))
|
|
840
|
-
text_chunk = None # release ref
|
|
841
753
|
|
|
842
|
-
def next_chunk(
|
|
843
|
-
self,
|
|
844
|
-
meta: CtxMeta,
|
|
845
|
-
ctx: CtxItem,
|
|
846
|
-
):
|
|
754
|
+
def next_chunk(self, meta: CtxMeta, ctx: CtxItem):
|
|
847
755
|
"""
|
|
848
756
|
Flush current stream and start with new chunks
|
|
849
757
|
|
|
@@ -851,7 +759,6 @@ class Renderer(BaseRenderer):
|
|
|
851
759
|
:param ctx: context item
|
|
852
760
|
"""
|
|
853
761
|
pid = self.get_or_create_pid(meta)
|
|
854
|
-
# Ensure all pending chunks are delivered before switching message
|
|
855
762
|
self._stream_flush(pid, force=True)
|
|
856
763
|
self.pids[pid].item = ctx
|
|
857
764
|
self.pids[pid].buffer = ""
|
|
@@ -864,26 +771,19 @@ class Renderer(BaseRenderer):
|
|
|
864
771
|
)
|
|
865
772
|
except Exception:
|
|
866
773
|
pass
|
|
867
|
-
# Reset micro-batch header for the next message
|
|
868
774
|
try:
|
|
869
775
|
self.pids[pid].header = self.get_name_header(ctx, stream=True)
|
|
870
776
|
except Exception:
|
|
871
777
|
self.pids[pid].header = ""
|
|
872
778
|
|
|
873
|
-
def append_chunk_input(
|
|
874
|
-
self,
|
|
875
|
-
meta: CtxMeta,
|
|
876
|
-
ctx: CtxItem,
|
|
877
|
-
text_chunk: str,
|
|
878
|
-
begin: bool = False
|
|
879
|
-
):
|
|
779
|
+
def append_chunk_input(self, meta: CtxMeta, ctx: CtxItem, text_chunk: str, begin: bool = False):
|
|
880
780
|
"""
|
|
881
|
-
Append output chunk to
|
|
781
|
+
Append output chunk to input area (legacy)
|
|
882
782
|
|
|
883
783
|
:param meta: context meta
|
|
884
784
|
:param ctx: context item
|
|
885
|
-
:param text_chunk: text chunk
|
|
886
|
-
:param begin: if
|
|
785
|
+
:param text_chunk: text chunk to append
|
|
786
|
+
:param begin: True if begin of stream
|
|
887
787
|
"""
|
|
888
788
|
if not text_chunk:
|
|
889
789
|
return
|
|
@@ -891,28 +791,19 @@ class Renderer(BaseRenderer):
|
|
|
891
791
|
return
|
|
892
792
|
try:
|
|
893
793
|
self.get_output_node(meta).page().bridge.nodeInput.emit(
|
|
894
|
-
self.sanitize_html(
|
|
895
|
-
text_chunk
|
|
896
|
-
)
|
|
794
|
+
self.sanitize_html(text_chunk)
|
|
897
795
|
)
|
|
898
796
|
except Exception:
|
|
899
797
|
pass
|
|
900
|
-
text_chunk = None # free reference
|
|
901
798
|
|
|
902
|
-
def append_live(
|
|
903
|
-
self,
|
|
904
|
-
meta: CtxMeta,
|
|
905
|
-
ctx: CtxItem,
|
|
906
|
-
text_chunk: str,
|
|
907
|
-
begin: bool = False
|
|
908
|
-
):
|
|
799
|
+
def append_live(self, meta: CtxMeta, ctx: CtxItem, text_chunk: str, begin: bool = False):
|
|
909
800
|
"""
|
|
910
|
-
Append live output chunk to output
|
|
801
|
+
Append live output chunk to output (legacy live preview)
|
|
911
802
|
|
|
912
803
|
:param meta: context meta
|
|
913
804
|
:param ctx: context item
|
|
914
|
-
:param text_chunk: text chunk
|
|
915
|
-
:param begin: if
|
|
805
|
+
:param text_chunk: text chunk to append
|
|
806
|
+
:param begin: True if begin of stream
|
|
916
807
|
"""
|
|
917
808
|
pid = self.get_or_create_pid(meta)
|
|
918
809
|
self.pids[pid].item = ctx
|
|
@@ -933,7 +824,6 @@ class Renderer(BaseRenderer):
|
|
|
933
824
|
self.pids[pid].is_cmd = False
|
|
934
825
|
self.clear_live(meta, ctx)
|
|
935
826
|
self.pids[pid].append_live_buffer(raw_chunk)
|
|
936
|
-
raw_chunk = None # free reference
|
|
937
827
|
|
|
938
828
|
try:
|
|
939
829
|
self.get_output_node(meta).page().runJavaScript(
|
|
@@ -943,7 +833,7 @@ class Renderer(BaseRenderer):
|
|
|
943
833
|
)
|
|
944
834
|
)});"""
|
|
945
835
|
)
|
|
946
|
-
except Exception
|
|
836
|
+
except Exception:
|
|
947
837
|
pass
|
|
948
838
|
|
|
949
839
|
def clear_live(self, meta: CtxMeta, ctx: CtxItem):
|
|
@@ -963,22 +853,15 @@ class Renderer(BaseRenderer):
|
|
|
963
853
|
except Exception:
|
|
964
854
|
pass
|
|
965
855
|
|
|
966
|
-
def append_node(
|
|
967
|
-
|
|
968
|
-
meta: CtxMeta,
|
|
969
|
-
ctx: CtxItem,
|
|
970
|
-
html: str,
|
|
971
|
-
type: int = 1,
|
|
972
|
-
prev_ctx: Optional[CtxItem] = None,
|
|
973
|
-
next_ctx: Optional[CtxItem] = None
|
|
974
|
-
):
|
|
856
|
+
def append_node(self, meta: CtxMeta, ctx: CtxItem, html: str, type: int = 1,
|
|
857
|
+
prev_ctx: Optional[CtxItem] = None, next_ctx: Optional[CtxItem] = None):
|
|
975
858
|
"""
|
|
976
|
-
|
|
859
|
+
Backward compatible: when called, convert HTML-like path to RenderBlock and send JSON.
|
|
977
860
|
|
|
978
861
|
:param meta: context meta
|
|
979
|
-
:param
|
|
980
|
-
:param
|
|
981
|
-
:param
|
|
862
|
+
:param ctx: context item
|
|
863
|
+
:param html: HTML content
|
|
864
|
+
:param type: NODE_INPUT or NODE_OUTPUT
|
|
982
865
|
:param prev_ctx: previous context item
|
|
983
866
|
:param next_ctx: next context item
|
|
984
867
|
"""
|
|
@@ -986,86 +869,57 @@ class Renderer(BaseRenderer):
|
|
|
986
869
|
return
|
|
987
870
|
|
|
988
871
|
pid = self.get_or_create_pid(meta)
|
|
989
|
-
self.append(
|
|
990
|
-
pid,
|
|
991
|
-
self.prepare_node(
|
|
992
|
-
meta=meta,
|
|
993
|
-
ctx=ctx,
|
|
994
|
-
html=html,
|
|
995
|
-
type=type,
|
|
996
|
-
prev_ctx=prev_ctx,
|
|
997
|
-
next_ctx=next_ctx,
|
|
998
|
-
)
|
|
999
|
-
)
|
|
1000
872
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
873
|
+
# Convert to RenderBlock JSON
|
|
874
|
+
input_text = html if type == self.NODE_INPUT else None
|
|
875
|
+
output_text = html if type == self.NODE_OUTPUT else None
|
|
876
|
+
block = self._build_render_block(meta, ctx, input_text, output_text, prev_ctx=prev_ctx, next_ctx=next_ctx)
|
|
877
|
+
if block:
|
|
878
|
+
self.append(pid, block.to_json(wrap=True))
|
|
879
|
+
|
|
880
|
+
def append(self, pid, payload: str, flush: bool = False, replace: bool = False):
|
|
1008
881
|
"""
|
|
1009
|
-
Append
|
|
882
|
+
Append payload (HTML legacy or JSON string) to output.
|
|
1010
883
|
|
|
1011
|
-
:param pid:
|
|
1012
|
-
:param
|
|
1013
|
-
:param flush: True if flush
|
|
1014
|
-
:param replace: True if replace current content
|
|
884
|
+
:param pid: context PID
|
|
885
|
+
:param payload: payload to append
|
|
886
|
+
:param flush: True if flush immediately (legacy HTML path)
|
|
1015
887
|
"""
|
|
1016
888
|
if self.pids[pid].loaded and not self.pids[pid].use_buffer:
|
|
1017
889
|
self.clear_chunks(pid)
|
|
1018
|
-
if
|
|
1019
|
-
self.flush_output(pid,
|
|
890
|
+
if payload:
|
|
891
|
+
self.flush_output(pid, payload, replace)
|
|
1020
892
|
self.pids[pid].clear()
|
|
1021
893
|
else:
|
|
1022
894
|
if not flush:
|
|
1023
|
-
self.pids[pid].append_html(
|
|
1024
|
-
html = None # free reference
|
|
895
|
+
self.pids[pid].append_html(payload)
|
|
1025
896
|
|
|
1026
|
-
def append_context_item(
|
|
1027
|
-
|
|
1028
|
-
meta: CtxMeta,
|
|
1029
|
-
ctx: CtxItem,
|
|
1030
|
-
prev_ctx: Optional[CtxItem] = None,
|
|
1031
|
-
next_ctx: Optional[CtxItem] = None
|
|
1032
|
-
):
|
|
897
|
+
def append_context_item(self, meta: CtxMeta, ctx: CtxItem,
|
|
898
|
+
prev_ctx: Optional[CtxItem] = None, next_ctx: Optional[CtxItem] = None):
|
|
1033
899
|
"""
|
|
1034
|
-
Append context item
|
|
900
|
+
Append context item as one RenderBlock with input+output (if present)
|
|
1035
901
|
|
|
1036
902
|
:param meta: context meta
|
|
1037
903
|
:param ctx: context item
|
|
1038
904
|
:param prev_ctx: previous context item
|
|
1039
905
|
:param next_ctx: next context item
|
|
1040
906
|
"""
|
|
1041
|
-
self.
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
flush=False,
|
|
1050
|
-
prev_ctx=prev_ctx,
|
|
1051
|
-
next_ctx=next_ctx,
|
|
1052
|
-
)
|
|
1053
|
-
|
|
1054
|
-
def append_extra(
|
|
1055
|
-
self,
|
|
1056
|
-
meta: CtxMeta,
|
|
1057
|
-
ctx: CtxItem,
|
|
1058
|
-
footer: bool = False,
|
|
1059
|
-
render: bool = True
|
|
1060
|
-
) -> str:
|
|
907
|
+
input_text = self.prepare_input(meta, ctx, flush=False, append=False)
|
|
908
|
+
output_text = self.prepare_output(meta, ctx, flush=False, prev_ctx=prev_ctx, next_ctx=next_ctx)
|
|
909
|
+
block = self._build_render_block(meta, ctx, input_text, output_text, prev_ctx=prev_ctx, next_ctx=next_ctx)
|
|
910
|
+
if block:
|
|
911
|
+
pid = self.get_or_create_pid(meta)
|
|
912
|
+
self.append(pid, block.to_json(wrap=True))
|
|
913
|
+
|
|
914
|
+
def append_extra(self, meta: CtxMeta, ctx: CtxItem, footer: bool = False, render: bool = True) -> str:
|
|
1061
915
|
"""
|
|
1062
|
-
Append extra data (
|
|
916
|
+
Append extra data (legacy HTML way) – kept for runtime calls that append later.
|
|
1063
917
|
|
|
1064
|
-
:param meta:
|
|
918
|
+
:param meta: context meta
|
|
1065
919
|
:param ctx: context item
|
|
1066
|
-
:param footer: True if
|
|
1067
|
-
:param render: True if render
|
|
1068
|
-
:return: HTML
|
|
920
|
+
:param footer: True if append at the end (legacy)
|
|
921
|
+
:param render: True if render to output (legacy)
|
|
922
|
+
:return: rendered HTML
|
|
1069
923
|
"""
|
|
1070
924
|
self.tool_output_end()
|
|
1071
925
|
|
|
@@ -1086,7 +940,7 @@ class Renderer(BaseRenderer):
|
|
|
1086
940
|
html_parts.append(self.body.get_image_html(image, n, c))
|
|
1087
941
|
self.pids[pid].images_appended.append(image)
|
|
1088
942
|
n += 1
|
|
1089
|
-
except Exception
|
|
943
|
+
except Exception:
|
|
1090
944
|
pass
|
|
1091
945
|
|
|
1092
946
|
c = len(ctx.files)
|
|
@@ -1101,7 +955,7 @@ class Renderer(BaseRenderer):
|
|
|
1101
955
|
files_html.append(self.body.get_file_html(file, n, c))
|
|
1102
956
|
self.pids[pid].files_appended.append(file)
|
|
1103
957
|
n += 1
|
|
1104
|
-
except Exception
|
|
958
|
+
except Exception:
|
|
1105
959
|
pass
|
|
1106
960
|
if files_html:
|
|
1107
961
|
html_parts.append("<br/><br/>".join(files_html))
|
|
@@ -1118,7 +972,7 @@ class Renderer(BaseRenderer):
|
|
|
1118
972
|
urls_html.append(self.body.get_url_html(url, n, c))
|
|
1119
973
|
self.pids[pid].urls_appended.append(url)
|
|
1120
974
|
n += 1
|
|
1121
|
-
except Exception
|
|
975
|
+
except Exception:
|
|
1122
976
|
pass
|
|
1123
977
|
if urls_html:
|
|
1124
978
|
html_parts.append("<br/><br/>".join(urls_html))
|
|
@@ -1128,7 +982,7 @@ class Renderer(BaseRenderer):
|
|
|
1128
982
|
try:
|
|
1129
983
|
docs = self.body.get_docs_html(ctx.doc_ids)
|
|
1130
984
|
html_parts.append(docs)
|
|
1131
|
-
except Exception
|
|
985
|
+
except Exception:
|
|
1132
986
|
pass
|
|
1133
987
|
|
|
1134
988
|
html = "".join(html_parts)
|
|
@@ -1142,24 +996,19 @@ class Renderer(BaseRenderer):
|
|
|
1142
996
|
self.sanitize_html(html)
|
|
1143
997
|
)});"""
|
|
1144
998
|
)
|
|
1145
|
-
except Exception
|
|
999
|
+
except Exception:
|
|
1146
1000
|
pass
|
|
1147
1001
|
|
|
1148
1002
|
return html
|
|
1149
1003
|
|
|
1150
|
-
def append_timestamp(
|
|
1151
|
-
self,
|
|
1152
|
-
ctx: CtxItem,
|
|
1153
|
-
text: str,
|
|
1154
|
-
type: Optional[int] = None
|
|
1155
|
-
) -> str:
|
|
1004
|
+
def append_timestamp(self, ctx: CtxItem, text: str, type: Optional[int] = None) -> str:
|
|
1156
1005
|
"""
|
|
1157
|
-
Append timestamp to text
|
|
1006
|
+
Append timestamp to text (legacy HTML path)
|
|
1158
1007
|
|
|
1159
1008
|
:param ctx: context item
|
|
1160
|
-
:param text:
|
|
1161
|
-
:param type:
|
|
1162
|
-
:return:
|
|
1009
|
+
:param text: text to append timestamp to
|
|
1010
|
+
:param type: NODE_INPUT or NODE_OUTPUT
|
|
1011
|
+
:return: text with timestamp
|
|
1163
1012
|
"""
|
|
1164
1013
|
if ctx is not None and ctx.input_timestamp is not None:
|
|
1165
1014
|
timestamp = None
|
|
@@ -1173,16 +1022,12 @@ class Renderer(BaseRenderer):
|
|
|
1173
1022
|
text = f'<span class="ts">{hour}: </span>{text}'
|
|
1174
1023
|
return text
|
|
1175
1024
|
|
|
1176
|
-
def reset(
|
|
1177
|
-
self,
|
|
1178
|
-
meta: Optional[CtxMeta] = None,
|
|
1179
|
-
clear_nodes: bool = True
|
|
1180
|
-
):
|
|
1025
|
+
def reset(self, meta: Optional[CtxMeta] = None, clear_nodes: bool = True):
|
|
1181
1026
|
"""
|
|
1182
|
-
Reset
|
|
1027
|
+
Reset current output
|
|
1183
1028
|
|
|
1184
|
-
:param meta:
|
|
1185
|
-
:param clear_nodes: True if clear nodes
|
|
1029
|
+
:param meta: context meta
|
|
1030
|
+
:param clear_nodes: True if clear nodes list
|
|
1186
1031
|
"""
|
|
1187
1032
|
pid = self.get_pid(meta)
|
|
1188
1033
|
if pid is not None and pid in self.pids:
|
|
@@ -1191,7 +1036,6 @@ class Renderer(BaseRenderer):
|
|
|
1191
1036
|
if meta is not None:
|
|
1192
1037
|
pid = self.get_or_create_pid(meta)
|
|
1193
1038
|
self.reset_by_pid(pid, clear_nodes=clear_nodes)
|
|
1194
|
-
|
|
1195
1039
|
self.clear_live(meta, CtxItem())
|
|
1196
1040
|
|
|
1197
1041
|
def reset_by_pid(self, pid: Optional[int], clear_nodes: bool = True):
|
|
@@ -1199,7 +1043,7 @@ class Renderer(BaseRenderer):
|
|
|
1199
1043
|
Reset by PID
|
|
1200
1044
|
|
|
1201
1045
|
:param pid: context PID
|
|
1202
|
-
:param clear_nodes: True if clear nodes
|
|
1046
|
+
:param clear_nodes: True if clear nodes list
|
|
1203
1047
|
"""
|
|
1204
1048
|
self.pids[pid].item = None
|
|
1205
1049
|
self.pids[pid].html = ""
|
|
@@ -1214,27 +1058,24 @@ class Renderer(BaseRenderer):
|
|
|
1214
1058
|
node.reset_current_content()
|
|
1215
1059
|
self.reset_names_by_pid(pid)
|
|
1216
1060
|
self.prev_chunk_replace = False
|
|
1217
|
-
self._stream_reset(pid)
|
|
1061
|
+
self._stream_reset(pid)
|
|
1218
1062
|
|
|
1219
1063
|
def clear_input(self):
|
|
1220
1064
|
"""Clear input"""
|
|
1221
1065
|
self.get_input_node().clear()
|
|
1222
1066
|
|
|
1223
|
-
def clear_output(
|
|
1224
|
-
self,
|
|
1225
|
-
meta: Optional[CtxMeta] = None
|
|
1226
|
-
):
|
|
1067
|
+
def clear_output(self, meta: Optional[CtxMeta] = None):
|
|
1227
1068
|
"""
|
|
1228
1069
|
Clear output
|
|
1229
1070
|
|
|
1230
|
-
:param meta:
|
|
1071
|
+
:param meta: context meta
|
|
1231
1072
|
"""
|
|
1232
1073
|
self.prev_chunk_replace = False
|
|
1233
1074
|
self.reset(meta)
|
|
1234
1075
|
|
|
1235
|
-
def clear_chunks(self, pid):
|
|
1076
|
+
def clear_chunks(self, pid: Optional[int]):
|
|
1236
1077
|
"""
|
|
1237
|
-
Clear chunks
|
|
1078
|
+
Clear current chunks
|
|
1238
1079
|
|
|
1239
1080
|
:param pid: context PID
|
|
1240
1081
|
"""
|
|
@@ -1243,14 +1084,11 @@ class Renderer(BaseRenderer):
|
|
|
1243
1084
|
self.clear_chunks_input(pid)
|
|
1244
1085
|
self.clear_chunks_output(pid)
|
|
1245
1086
|
|
|
1246
|
-
def clear_chunks_input(
|
|
1247
|
-
self,
|
|
1248
|
-
pid: Optional[int]
|
|
1249
|
-
):
|
|
1087
|
+
def clear_chunks_input(self, pid: Optional[int]):
|
|
1250
1088
|
"""
|
|
1251
1089
|
Clear chunks from input
|
|
1252
1090
|
|
|
1253
|
-
:pid: context PID
|
|
1091
|
+
:param pid: context PID
|
|
1254
1092
|
"""
|
|
1255
1093
|
if pid is None:
|
|
1256
1094
|
return
|
|
@@ -1261,14 +1099,11 @@ class Renderer(BaseRenderer):
|
|
|
1261
1099
|
except Exception:
|
|
1262
1100
|
pass
|
|
1263
1101
|
|
|
1264
|
-
def clear_chunks_output(
|
|
1265
|
-
self,
|
|
1266
|
-
pid: Optional[int]
|
|
1267
|
-
):
|
|
1102
|
+
def clear_chunks_output(self, pid: Optional[int]):
|
|
1268
1103
|
"""
|
|
1269
1104
|
Clear chunks from output
|
|
1270
1105
|
|
|
1271
|
-
:pid: context PID
|
|
1106
|
+
:param pid: context PID
|
|
1272
1107
|
"""
|
|
1273
1108
|
self.prev_chunk_replace = False
|
|
1274
1109
|
try:
|
|
@@ -1277,16 +1112,13 @@ class Renderer(BaseRenderer):
|
|
|
1277
1112
|
)
|
|
1278
1113
|
except Exception:
|
|
1279
1114
|
pass
|
|
1280
|
-
self._stream_reset(pid)
|
|
1115
|
+
self._stream_reset(pid)
|
|
1281
1116
|
|
|
1282
|
-
def clear_nodes(
|
|
1283
|
-
self,
|
|
1284
|
-
pid: Optional[int]
|
|
1285
|
-
):
|
|
1117
|
+
def clear_nodes(self, pid: Optional[int]):
|
|
1286
1118
|
"""
|
|
1287
|
-
Clear nodes
|
|
1119
|
+
Clear nodes list
|
|
1288
1120
|
|
|
1289
|
-
:pid: context PID
|
|
1121
|
+
:param pid: context PID
|
|
1290
1122
|
"""
|
|
1291
1123
|
try:
|
|
1292
1124
|
self.get_output_node_by_pid(pid).page().runJavaScript(
|
|
@@ -1295,173 +1127,33 @@ class Renderer(BaseRenderer):
|
|
|
1295
1127
|
except Exception:
|
|
1296
1128
|
pass
|
|
1297
1129
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
ctx: CtxItem,
|
|
1302
|
-
html: str,
|
|
1303
|
-
type: int = 1,
|
|
1304
|
-
prev_ctx: Optional[CtxItem] = None,
|
|
1305
|
-
next_ctx: Optional[CtxItem] = None
|
|
1306
|
-
) -> str:
|
|
1130
|
+
# Legacy methods kept for compatibility with existing code paths
|
|
1131
|
+
def prepare_node(self, meta: CtxMeta, ctx: CtxItem, html: str, type: int = 1,
|
|
1132
|
+
prev_ctx: Optional[CtxItem] = None, next_ctx: Optional[CtxItem] = None) -> str:
|
|
1307
1133
|
"""
|
|
1308
|
-
|
|
1134
|
+
Compatibility shim: convert single input/output into markdown/raw for JS templates.
|
|
1309
1135
|
|
|
1310
1136
|
:param meta: context meta
|
|
1311
|
-
:param ctx:
|
|
1312
|
-
:param html:
|
|
1313
|
-
:param type:
|
|
1137
|
+
:param ctx: context item
|
|
1138
|
+
:param html: HTML content
|
|
1139
|
+
:param type: NODE_INPUT or NODE_OUTPUT
|
|
1314
1140
|
:param prev_ctx: previous context item
|
|
1315
1141
|
:param next_ctx: next context item
|
|
1316
|
-
:return: prepared
|
|
1142
|
+
:return: prepared text
|
|
1317
1143
|
"""
|
|
1318
1144
|
pid = self.get_or_create_pid(meta)
|
|
1319
1145
|
if type == self.NODE_OUTPUT:
|
|
1320
|
-
return
|
|
1321
|
-
meta=meta,
|
|
1322
|
-
ctx=ctx,
|
|
1323
|
-
html=html,
|
|
1324
|
-
prev_ctx=prev_ctx,
|
|
1325
|
-
next_ctx=next_ctx,
|
|
1326
|
-
)
|
|
1146
|
+
return str(html)
|
|
1327
1147
|
elif type == self.NODE_INPUT:
|
|
1328
|
-
return
|
|
1329
|
-
pid=pid,
|
|
1330
|
-
ctx=ctx,
|
|
1331
|
-
html=html,
|
|
1332
|
-
prev_ctx=prev_ctx,
|
|
1333
|
-
next_ctx=next_ctx,
|
|
1334
|
-
)
|
|
1335
|
-
|
|
1336
|
-
def prepare_node_input(
|
|
1337
|
-
self,
|
|
1338
|
-
pid,
|
|
1339
|
-
ctx: CtxItem,
|
|
1340
|
-
html: str,
|
|
1341
|
-
prev_ctx: Optional[CtxItem] = None,
|
|
1342
|
-
next_ctx: Optional[CtxItem] = None
|
|
1343
|
-
) -> str:
|
|
1344
|
-
"""
|
|
1345
|
-
Prepare input node
|
|
1346
|
-
|
|
1347
|
-
:param pid: context PID
|
|
1348
|
-
:param ctx: CtxItem instance
|
|
1349
|
-
:param html: html text
|
|
1350
|
-
:param prev_ctx: previous context item
|
|
1351
|
-
:param next_ctx: next context item
|
|
1352
|
-
:return: prepared HTML
|
|
1353
|
-
"""
|
|
1354
|
-
msg_id = "msg-user-" + str(ctx.id) if ctx is not None else ""
|
|
1355
|
-
content = self.append_timestamp(
|
|
1356
|
-
ctx,
|
|
1357
|
-
self.helpers.format_user_text(html),
|
|
1358
|
-
type=self.NODE_INPUT
|
|
1359
|
-
)
|
|
1360
|
-
html = f"<p>{content}</p>"
|
|
1361
|
-
html = self.helpers.post_format_text(html)
|
|
1362
|
-
name = self.pids[pid].name_user
|
|
1363
|
-
|
|
1364
|
-
if ctx.internal and ctx.input.startswith("[{"):
|
|
1365
|
-
name = trans("msg.name.system")
|
|
1366
|
-
if type(ctx.extra) is dict and "agent_evaluate" in ctx.extra:
|
|
1367
|
-
name = trans("msg.name.evaluation")
|
|
1368
|
-
|
|
1369
|
-
debug = ""
|
|
1370
|
-
if self.is_debug():
|
|
1371
|
-
debug = self.append_debug(ctx, pid, "input")
|
|
1372
|
-
|
|
1373
|
-
extra_style = ""
|
|
1374
|
-
extra = ""
|
|
1375
|
-
if ctx.extra is not None and "footer" in ctx.extra:
|
|
1376
|
-
extra = ctx.extra["footer"]
|
|
1377
|
-
extra_style = "display:block;"
|
|
1378
|
-
|
|
1379
|
-
return f'<div class="msg-box msg-user" id="{msg_id}"><div class="name-header name-user">{name}</div><div class="msg">{html}<div class="msg-extra" style="{extra_style}">{extra}</div>{debug}</div></div>'
|
|
1380
|
-
|
|
1381
|
-
def prepare_node_output(
|
|
1382
|
-
self,
|
|
1383
|
-
meta: CtxMeta,
|
|
1384
|
-
ctx: CtxItem,
|
|
1385
|
-
html: str,
|
|
1386
|
-
prev_ctx: Optional[CtxItem] = None,
|
|
1387
|
-
next_ctx: Optional[CtxItem] = None
|
|
1388
|
-
) -> str:
|
|
1389
|
-
"""
|
|
1390
|
-
Prepare output node wrapper; content stays as raw Markdown in HTML (md-block-markdown=1).
|
|
1391
|
-
JS (markdown-it) will render it and decorate code blocks.
|
|
1392
|
-
Backward compatible with previous JS pipeline and public API.
|
|
1393
|
-
|
|
1394
|
-
:param meta: context meta
|
|
1395
|
-
:param ctx: CtxItem instance
|
|
1396
|
-
:param html: raw markdown text (pre/post formatted)
|
|
1397
|
-
:param prev_ctx: previous context item
|
|
1398
|
-
:param next_ctx: next context item
|
|
1399
|
-
:return: prepared HTML
|
|
1400
|
-
"""
|
|
1401
|
-
is_cmd = (
|
|
1402
|
-
next_ctx is not None and
|
|
1403
|
-
next_ctx.internal and
|
|
1404
|
-
(len(ctx.cmds) > 0 or (ctx.extra_ctx is not None and len(ctx.extra_ctx) > 0))
|
|
1405
|
-
)
|
|
1406
|
-
|
|
1407
|
-
pid = self.get_or_create_pid(meta)
|
|
1408
|
-
msg_id = f"msg-bot-{ctx.id}" if ctx is not None else ""
|
|
1409
|
-
|
|
1410
|
-
# raw Markdown (no Python-side HTML rendering); keep existing pre/post formatting hooks
|
|
1411
|
-
md_src = self.helpers.pre_format_text(html)
|
|
1412
|
-
md_text = self.helpers.post_format_text(md_src)
|
|
1413
|
-
|
|
1414
|
-
# Escape Markdown for safe inclusion as textContent (browser will decode entities back to chars)
|
|
1415
|
-
# This ensures no HTML is parsed before markdown-it processes the content on the JS side.
|
|
1416
|
-
md_text_escaped = _html.escape(md_text, quote=False)
|
|
1417
|
-
|
|
1418
|
-
# extras/footer
|
|
1419
|
-
extra = self.append_extra(meta, ctx, footer=True, render=False)
|
|
1420
|
-
footer = self.body.prepare_action_icons(ctx)
|
|
1421
|
-
|
|
1422
|
-
tool_output = ""
|
|
1423
|
-
output_class = "display:none"
|
|
1424
|
-
if is_cmd:
|
|
1425
|
-
if ctx.results is not None and len(ctx.results) > 0 \
|
|
1426
|
-
and isinstance(ctx.extra, dict) and "agent_step" in ctx.extra:
|
|
1427
|
-
tool_output = self.helpers.format_cmd_text(str(ctx.input), indent=True)
|
|
1428
|
-
output_class = ""
|
|
1429
|
-
else:
|
|
1430
|
-
tool_output = self.helpers.format_cmd_text(str(next_ctx.input), indent=True)
|
|
1431
|
-
output_class = ""
|
|
1432
|
-
elif ctx.results is not None and len(ctx.results) > 0 \
|
|
1433
|
-
and isinstance(ctx.extra, dict) and "agent_step" in ctx.extra:
|
|
1434
|
-
tool_output = self.helpers.format_cmd_text(str(ctx.input), indent=True)
|
|
1435
|
-
|
|
1436
|
-
tool_extra = self.body.prepare_tool_extra(ctx)
|
|
1437
|
-
debug = self.append_debug(ctx, pid, "output") if self.is_debug() else ""
|
|
1438
|
-
name_header = self.get_name_header(ctx)
|
|
1439
|
-
|
|
1440
|
-
# Native Markdown block: JS runtime will pick [md-block-markdown] and render via markdown-it.
|
|
1441
|
-
return (
|
|
1442
|
-
f"<div class='msg-box msg-bot' id='{msg_id}'>"
|
|
1443
|
-
f"{name_header}"
|
|
1444
|
-
f"<div class='msg'>"
|
|
1445
|
-
f"<div class='md-block' md-block-markdown='1'>{md_text_escaped}</div>"
|
|
1446
|
-
f"<div class='msg-tool-extra'>{tool_extra}</div>"
|
|
1447
|
-
f"<div class='tool-output' style='{output_class}'>"
|
|
1448
|
-
f"<span class='toggle-cmd-output' onclick='toggleToolOutput({ctx.id});' "
|
|
1449
|
-
f"title='{trans('action.cmd.expand')}' role='button'>"
|
|
1450
|
-
f"<img src='{self._file_prefix}{self._icon_expand}' width='25' height='25' valign='middle'></span>"
|
|
1451
|
-
f"<div class='content' style='display:none'>{tool_output}</div>"
|
|
1452
|
-
f"</div>"
|
|
1453
|
-
f"<div class='msg-extra'>{extra}</div>"
|
|
1454
|
-
f"{footer}{debug}"
|
|
1455
|
-
f"</div></div>"
|
|
1456
|
-
)
|
|
1148
|
+
return str(html)
|
|
1457
1149
|
|
|
1458
1150
|
def get_name_header(self, ctx: CtxItem, stream: bool = False) -> str:
|
|
1459
1151
|
"""
|
|
1460
|
-
|
|
1152
|
+
Legacy - kept for stream header text (avatar + name string)
|
|
1461
1153
|
|
|
1462
|
-
:param ctx:
|
|
1463
|
-
:param stream: True if
|
|
1464
|
-
:return: HTML
|
|
1154
|
+
:param ctx: context item
|
|
1155
|
+
:param stream: True if streaming mode
|
|
1156
|
+
:return: HTML string
|
|
1465
1157
|
"""
|
|
1466
1158
|
meta = ctx.meta
|
|
1467
1159
|
if meta is None:
|
|
@@ -1494,13 +1186,13 @@ class Renderer(BaseRenderer):
|
|
|
1494
1186
|
else:
|
|
1495
1187
|
return f"<div class=\"name-header name-bot\">{avatar_html}{output_name}</div>"
|
|
1496
1188
|
|
|
1497
|
-
def flush_output(self, pid: int,
|
|
1189
|
+
def flush_output(self, pid: int, payload: str, replace: bool = False):
|
|
1498
1190
|
"""
|
|
1499
|
-
Send content via QWebChannel
|
|
1191
|
+
Send content via QWebChannel (JSON or HTML string).
|
|
1500
1192
|
|
|
1501
1193
|
:param pid: context PID
|
|
1502
|
-
:param
|
|
1503
|
-
:param replace: True if replace
|
|
1194
|
+
:param payload: payload to send
|
|
1195
|
+
:param replace: True if replace nodes list
|
|
1504
1196
|
"""
|
|
1505
1197
|
if pid is None:
|
|
1506
1198
|
return
|
|
@@ -1516,23 +1208,23 @@ class Renderer(BaseRenderer):
|
|
|
1516
1208
|
if br is not None:
|
|
1517
1209
|
if replace and hasattr(br, "nodeReplace"):
|
|
1518
1210
|
self.clear_nodes(pid)
|
|
1519
|
-
br.nodeReplace.emit(
|
|
1211
|
+
br.nodeReplace.emit(payload)
|
|
1520
1212
|
return
|
|
1521
1213
|
if not replace and hasattr(br, "node"):
|
|
1522
|
-
br.node.emit(
|
|
1214
|
+
br.node.emit(payload)
|
|
1523
1215
|
return
|
|
1524
|
-
# Not ready
|
|
1525
|
-
self._queue_node(pid,
|
|
1216
|
+
# Not ready -> queue
|
|
1217
|
+
self._queue_node(pid, payload, replace)
|
|
1526
1218
|
except Exception:
|
|
1527
|
-
#
|
|
1219
|
+
# Fallback to runJavaScript path
|
|
1528
1220
|
try:
|
|
1529
1221
|
if replace:
|
|
1530
1222
|
node.page().runJavaScript(
|
|
1531
|
-
f"if (typeof window.replaceNodes !== 'undefined') replaceNodes({self.to_json(
|
|
1223
|
+
f"if (typeof window.replaceNodes !== 'undefined') replaceNodes({self.to_json(payload)});"
|
|
1532
1224
|
)
|
|
1533
1225
|
else:
|
|
1534
1226
|
node.page().runJavaScript(
|
|
1535
|
-
f"if (typeof window.appendNode !== 'undefined') appendNode({self.to_json(
|
|
1227
|
+
f"if (typeof window.appendNode !== 'undefined') appendNode({self.to_json(payload)});"
|
|
1536
1228
|
)
|
|
1537
1229
|
except Exception:
|
|
1538
1230
|
pass
|
|
@@ -1541,13 +1233,9 @@ class Renderer(BaseRenderer):
|
|
|
1541
1233
|
"""Reload output, called externally only on theme change to redraw content"""
|
|
1542
1234
|
self.window.controller.ctx.refresh_output()
|
|
1543
1235
|
|
|
1544
|
-
def flush(
|
|
1545
|
-
self,
|
|
1546
|
-
pid:
|
|
1547
|
-
Optional[int]
|
|
1548
|
-
):
|
|
1236
|
+
def flush(self, pid: Optional[int]):
|
|
1549
1237
|
"""
|
|
1550
|
-
Flush output
|
|
1238
|
+
Flush output page HTML (initial)
|
|
1551
1239
|
|
|
1552
1240
|
:param pid: context PID
|
|
1553
1241
|
"""
|
|
@@ -1559,20 +1247,16 @@ class Renderer(BaseRenderer):
|
|
|
1559
1247
|
if node is not None:
|
|
1560
1248
|
node.setHtml(html, baseUrl="file://")
|
|
1561
1249
|
|
|
1562
|
-
def fresh(
|
|
1563
|
-
self,
|
|
1564
|
-
meta: Optional[CtxMeta] = None,
|
|
1565
|
-
force: bool = False
|
|
1566
|
-
):
|
|
1250
|
+
def fresh(self, meta: Optional[CtxMeta] = None, force: bool = False):
|
|
1567
1251
|
"""
|
|
1568
1252
|
Reset page / unload old renderer from memory
|
|
1569
1253
|
|
|
1570
1254
|
:param meta: context meta
|
|
1571
|
-
:param force: True if force
|
|
1255
|
+
:param force: True if force even when not needed
|
|
1572
1256
|
"""
|
|
1573
1257
|
plain = self.window.core.config.get('render.plain')
|
|
1574
1258
|
if plain:
|
|
1575
|
-
return
|
|
1259
|
+
return
|
|
1576
1260
|
|
|
1577
1261
|
pid = self.get_or_create_pid(meta)
|
|
1578
1262
|
if pid is None:
|
|
@@ -1599,26 +1283,20 @@ class Renderer(BaseRenderer):
|
|
|
1599
1283
|
self.recycle(node, meta)
|
|
1600
1284
|
self.flush(pid)
|
|
1601
1285
|
|
|
1602
|
-
def recycle(
|
|
1603
|
-
self,
|
|
1604
|
-
node: ChatWebOutput,
|
|
1605
|
-
meta: Optional[CtxMeta] = None
|
|
1606
|
-
):
|
|
1286
|
+
def recycle(self, node: ChatWebOutput, meta: Optional[CtxMeta] = None):
|
|
1607
1287
|
"""
|
|
1608
|
-
Recycle renderer to
|
|
1609
|
-
|
|
1610
|
-
Swaps out the old QWebEngineView with a fresh instance.
|
|
1288
|
+
Recycle renderer to avoid leaks
|
|
1611
1289
|
|
|
1612
|
-
:param node: output node
|
|
1290
|
+
:param node: output node to recycle
|
|
1613
1291
|
:param meta: context meta
|
|
1614
1292
|
"""
|
|
1615
1293
|
tab = node.get_tab()
|
|
1616
|
-
layout = tab.child.layout()
|
|
1294
|
+
layout = tab.child.layout()
|
|
1617
1295
|
layout.removeWidget(node)
|
|
1618
1296
|
self.window.ui.nodes['output'].pop(tab.pid, None)
|
|
1619
1297
|
tab.child.delete_refs()
|
|
1620
1298
|
|
|
1621
|
-
node.on_delete()
|
|
1299
|
+
node.on_delete()
|
|
1622
1300
|
|
|
1623
1301
|
view = ChatWebOutput(self.window)
|
|
1624
1302
|
view.set_tab(tab)
|
|
@@ -1633,31 +1311,25 @@ class Renderer(BaseRenderer):
|
|
|
1633
1311
|
gc.collect()
|
|
1634
1312
|
except Exception:
|
|
1635
1313
|
pass
|
|
1636
|
-
self.auto_cleanup_soft(meta)
|
|
1314
|
+
self.auto_cleanup_soft(meta)
|
|
1637
1315
|
|
|
1638
|
-
def get_output_node(
|
|
1639
|
-
self,
|
|
1640
|
-
meta: Optional[CtxMeta] = None
|
|
1641
|
-
) -> Optional[ChatWebOutput]:
|
|
1316
|
+
def get_output_node(self, meta: Optional[CtxMeta] = None) -> Optional[ChatWebOutput]:
|
|
1642
1317
|
"""
|
|
1643
1318
|
Get output node
|
|
1644
1319
|
|
|
1645
1320
|
:param meta: context meta
|
|
1646
|
-
:return: output node
|
|
1321
|
+
:return: output node or None
|
|
1647
1322
|
"""
|
|
1648
1323
|
if self._get_output_node_by_meta is None:
|
|
1649
1324
|
self._get_output_node_by_meta = self.window.core.ctx.output.get_output_node_by_meta
|
|
1650
1325
|
return self._get_output_node_by_meta(meta)
|
|
1651
1326
|
|
|
1652
|
-
def get_output_node_by_pid(
|
|
1653
|
-
self,
|
|
1654
|
-
pid: Optional[int]
|
|
1655
|
-
) -> Optional[ChatWebOutput]:
|
|
1327
|
+
def get_output_node_by_pid(self, pid: Optional[int]) -> Optional[ChatWebOutput]:
|
|
1656
1328
|
"""
|
|
1657
1329
|
Get output node by PID
|
|
1658
1330
|
|
|
1659
|
-
:param pid: context
|
|
1660
|
-
:return: output node
|
|
1331
|
+
:param pid: context PID
|
|
1332
|
+
:return: output node or None
|
|
1661
1333
|
"""
|
|
1662
1334
|
if self._get_output_node_by_pid is None:
|
|
1663
1335
|
self._get_output_node_by_pid = self.window.core.ctx.output.get_output_node_by_pid
|
|
@@ -1701,7 +1373,7 @@ class Renderer(BaseRenderer):
|
|
|
1701
1373
|
"""
|
|
1702
1374
|
Reset names
|
|
1703
1375
|
|
|
1704
|
-
|
|
1376
|
+
:param meta: context meta
|
|
1705
1377
|
"""
|
|
1706
1378
|
if meta is None:
|
|
1707
1379
|
return
|
|
@@ -1712,7 +1384,7 @@ class Renderer(BaseRenderer):
|
|
|
1712
1384
|
"""
|
|
1713
1385
|
Reset names by PID
|
|
1714
1386
|
|
|
1715
|
-
:param pid: context
|
|
1387
|
+
:param pid: context PID
|
|
1716
1388
|
"""
|
|
1717
1389
|
self.pids[pid].name_user = trans("chat.name.user")
|
|
1718
1390
|
self.pids[pid].name_bot = trans("chat.name.bot")
|
|
@@ -1727,7 +1399,7 @@ class Renderer(BaseRenderer):
|
|
|
1727
1399
|
|
|
1728
1400
|
def on_edit_submit(self, ctx: CtxItem):
|
|
1729
1401
|
"""
|
|
1730
|
-
On
|
|
1402
|
+
On edit submit
|
|
1731
1403
|
|
|
1732
1404
|
:param ctx: context item
|
|
1733
1405
|
"""
|
|
@@ -1737,7 +1409,7 @@ class Renderer(BaseRenderer):
|
|
|
1737
1409
|
"""
|
|
1738
1410
|
On enable edit icons
|
|
1739
1411
|
|
|
1740
|
-
:param live: True if live
|
|
1412
|
+
:param live: True if live mode
|
|
1741
1413
|
"""
|
|
1742
1414
|
if not live:
|
|
1743
1415
|
return
|
|
@@ -1752,7 +1424,7 @@ class Renderer(BaseRenderer):
|
|
|
1752
1424
|
"""
|
|
1753
1425
|
On disable edit icons
|
|
1754
1426
|
|
|
1755
|
-
:param live: True if live
|
|
1427
|
+
:param live: True if live mode
|
|
1756
1428
|
"""
|
|
1757
1429
|
if not live:
|
|
1758
1430
|
return
|
|
@@ -1767,7 +1439,7 @@ class Renderer(BaseRenderer):
|
|
|
1767
1439
|
"""
|
|
1768
1440
|
On enable timestamp
|
|
1769
1441
|
|
|
1770
|
-
:param live: True if live
|
|
1442
|
+
:param live: True if live mode
|
|
1771
1443
|
"""
|
|
1772
1444
|
if not live:
|
|
1773
1445
|
return
|
|
@@ -1782,7 +1454,7 @@ class Renderer(BaseRenderer):
|
|
|
1782
1454
|
"""
|
|
1783
1455
|
On disable timestamp
|
|
1784
1456
|
|
|
1785
|
-
:param live: True if live
|
|
1457
|
+
:param live: True if live mode
|
|
1786
1458
|
"""
|
|
1787
1459
|
if not live:
|
|
1788
1460
|
return
|
|
@@ -1793,13 +1465,9 @@ class Renderer(BaseRenderer):
|
|
|
1793
1465
|
except Exception:
|
|
1794
1466
|
pass
|
|
1795
1467
|
|
|
1796
|
-
def update_names(
|
|
1797
|
-
self,
|
|
1798
|
-
meta: CtxMeta,
|
|
1799
|
-
ctx: CtxItem
|
|
1800
|
-
):
|
|
1468
|
+
def update_names(self, meta: CtxMeta, ctx: CtxItem):
|
|
1801
1469
|
"""
|
|
1802
|
-
Update names
|
|
1470
|
+
Update names from ctx
|
|
1803
1471
|
|
|
1804
1472
|
:param meta: context meta
|
|
1805
1473
|
:param ctx: context item
|
|
@@ -1813,7 +1481,7 @@ class Renderer(BaseRenderer):
|
|
|
1813
1481
|
self.pids[pid].name_bot = ctx.output_name
|
|
1814
1482
|
|
|
1815
1483
|
def clear_all(self):
|
|
1816
|
-
"""Clear all"""
|
|
1484
|
+
"""Clear all tabs"""
|
|
1817
1485
|
for pid in self.pids:
|
|
1818
1486
|
self.clear_chunks(pid)
|
|
1819
1487
|
self.clear_nodes(pid)
|
|
@@ -1821,33 +1489,23 @@ class Renderer(BaseRenderer):
|
|
|
1821
1489
|
self._stream_reset(pid)
|
|
1822
1490
|
|
|
1823
1491
|
def scroll_to_bottom(self):
|
|
1824
|
-
"""Scroll to bottom"""
|
|
1492
|
+
"""Scroll to bottom placeholder"""
|
|
1825
1493
|
pass
|
|
1826
1494
|
|
|
1827
1495
|
def append_block(self):
|
|
1828
|
-
"""Append block
|
|
1496
|
+
"""Append block placeholder"""
|
|
1829
1497
|
pass
|
|
1830
1498
|
|
|
1831
1499
|
def to_end(self, ctx: CtxItem):
|
|
1832
|
-
"""
|
|
1833
|
-
Move cursor to end of output
|
|
1834
|
-
|
|
1835
|
-
:param ctx: context item
|
|
1836
|
-
"""
|
|
1500
|
+
"""Move cursor to end placeholder"""
|
|
1837
1501
|
pass
|
|
1838
1502
|
|
|
1839
1503
|
def get_all_nodes(self) -> list:
|
|
1840
|
-
"""
|
|
1841
|
-
Return all registered nodes
|
|
1842
|
-
|
|
1843
|
-
:return: list of ChatOutput nodes (tabs)
|
|
1844
|
-
"""
|
|
1504
|
+
"""Return all registered nodes"""
|
|
1845
1505
|
return self.window.core.ctx.output.get_all()
|
|
1846
1506
|
|
|
1847
|
-
# TODO: on lang change
|
|
1848
|
-
|
|
1849
1507
|
def reload_css(self):
|
|
1850
|
-
"""Reload CSS
|
|
1508
|
+
"""Reload CSS – propagate theme and config to runtime"""
|
|
1851
1509
|
to_json = self.to_json(self.body.prepare_styles())
|
|
1852
1510
|
nodes = self.get_all_nodes()
|
|
1853
1511
|
for pid in self.pids:
|
|
@@ -1865,7 +1523,7 @@ class Renderer(BaseRenderer):
|
|
|
1865
1523
|
node.page().runJavaScript(
|
|
1866
1524
|
"if (typeof window.disableBlocks !== 'undefined') disableBlocks();"
|
|
1867
1525
|
)
|
|
1868
|
-
except Exception
|
|
1526
|
+
except Exception:
|
|
1869
1527
|
pass
|
|
1870
1528
|
return
|
|
1871
1529
|
|
|
@@ -1877,16 +1535,12 @@ class Renderer(BaseRenderer):
|
|
|
1877
1535
|
self.reload_css()
|
|
1878
1536
|
return
|
|
1879
1537
|
|
|
1880
|
-
def tool_output_append(
|
|
1881
|
-
self,
|
|
1882
|
-
meta: CtxMeta,
|
|
1883
|
-
content: str
|
|
1884
|
-
):
|
|
1538
|
+
def tool_output_append(self, meta: CtxMeta, content: str):
|
|
1885
1539
|
"""
|
|
1886
1540
|
Add tool output (append)
|
|
1887
1541
|
|
|
1888
1542
|
:param meta: context meta
|
|
1889
|
-
:param content: content
|
|
1543
|
+
:param content: content to append
|
|
1890
1544
|
"""
|
|
1891
1545
|
try:
|
|
1892
1546
|
self.get_output_node(meta).page().runJavaScript(
|
|
@@ -1897,16 +1551,12 @@ class Renderer(BaseRenderer):
|
|
|
1897
1551
|
except Exception:
|
|
1898
1552
|
pass
|
|
1899
1553
|
|
|
1900
|
-
def tool_output_update(
|
|
1901
|
-
self,
|
|
1902
|
-
meta: CtxMeta,
|
|
1903
|
-
content: str
|
|
1904
|
-
):
|
|
1554
|
+
def tool_output_update(self, meta: CtxMeta, content: str):
|
|
1905
1555
|
"""
|
|
1906
1556
|
Replace tool output
|
|
1907
1557
|
|
|
1908
1558
|
:param meta: context meta
|
|
1909
|
-
:param content: content
|
|
1559
|
+
:param content: content to set
|
|
1910
1560
|
"""
|
|
1911
1561
|
try:
|
|
1912
1562
|
self.get_output_node(meta).page().runJavaScript(
|
|
@@ -1954,10 +1604,9 @@ class Renderer(BaseRenderer):
|
|
|
1954
1604
|
|
|
1955
1605
|
def sanitize_html(self, html: str) -> str:
|
|
1956
1606
|
"""
|
|
1957
|
-
Sanitize
|
|
1607
|
+
Sanitize HTM
|
|
1958
1608
|
|
|
1959
|
-
:param html: HTML string
|
|
1960
|
-
:return: sanitized HTML string
|
|
1609
|
+
:param html: HTML string
|
|
1961
1610
|
"""
|
|
1962
1611
|
return html
|
|
1963
1612
|
if not html:
|
|
@@ -1966,35 +1615,30 @@ class Renderer(BaseRenderer):
|
|
|
1966
1615
|
return html
|
|
1967
1616
|
return self.RE_AMP_LT_GT.sub(r'&\1;', html)
|
|
1968
1617
|
|
|
1969
|
-
def append_debug(
|
|
1970
|
-
self,
|
|
1971
|
-
ctx: CtxItem,
|
|
1972
|
-
pid,
|
|
1973
|
-
title: Optional[str] = None
|
|
1974
|
-
) -> str:
|
|
1618
|
+
def append_debug(self, ctx: CtxItem, pid, title: Optional[str] = None) -> str:
|
|
1975
1619
|
"""
|
|
1976
|
-
Append debug info
|
|
1620
|
+
Append debug info HTML (legacy path)
|
|
1977
1621
|
|
|
1978
1622
|
:param ctx: context item
|
|
1979
1623
|
:param pid: context PID
|
|
1980
|
-
:param title:
|
|
1981
|
-
:return: HTML
|
|
1624
|
+
:param title: optional title
|
|
1625
|
+
:return: HTML string
|
|
1982
1626
|
"""
|
|
1983
1627
|
if title is None:
|
|
1984
1628
|
title = "debug"
|
|
1985
|
-
return f"<div class='debug'><b>{title}:</b> pid: {pid}, ctx: {ctx.
|
|
1629
|
+
return f"<div class='debug'><b>{title}:</b> pid: {pid}, ctx: {_html.escape(ctx.to_debug())}</div>"
|
|
1986
1630
|
|
|
1987
1631
|
def is_debug(self) -> bool:
|
|
1988
1632
|
"""
|
|
1989
|
-
Check
|
|
1633
|
+
Check debug flag
|
|
1990
1634
|
|
|
1991
|
-
:return: True if debug
|
|
1635
|
+
:return: True if debug enabled
|
|
1992
1636
|
"""
|
|
1993
1637
|
return self.window.core.config.get("debug.render", False)
|
|
1994
1638
|
|
|
1995
1639
|
def js_stream_queue_len(self, pid: int):
|
|
1996
1640
|
"""
|
|
1997
|
-
Ask
|
|
1641
|
+
Ask JS side for stream queue length
|
|
1998
1642
|
|
|
1999
1643
|
:param pid: context PID
|
|
2000
1644
|
"""
|
|
@@ -2012,13 +1656,12 @@ class Renderer(BaseRenderer):
|
|
|
2012
1656
|
|
|
2013
1657
|
def remove_pid(self, pid: int):
|
|
2014
1658
|
"""
|
|
2015
|
-
Remove PID
|
|
1659
|
+
Remove PID and clean resources
|
|
2016
1660
|
|
|
2017
1661
|
:param pid: context PID
|
|
2018
1662
|
"""
|
|
2019
1663
|
if pid in self.pids:
|
|
2020
1664
|
del self.pids[pid]
|
|
2021
|
-
# Clean micro-batch resources
|
|
2022
1665
|
self._stream_reset(pid)
|
|
2023
1666
|
t = self._stream_timer.pop(pid, None)
|
|
2024
1667
|
if t:
|
|
@@ -2032,7 +1675,7 @@ class Renderer(BaseRenderer):
|
|
|
2032
1675
|
|
|
2033
1676
|
def on_js_ready(self, pid: int) -> None:
|
|
2034
1677
|
"""
|
|
2035
|
-
|
|
1678
|
+
Called by JS via bridge when ready
|
|
2036
1679
|
|
|
2037
1680
|
:param pid: context PID
|
|
2038
1681
|
"""
|
|
@@ -2044,7 +1687,7 @@ class Renderer(BaseRenderer):
|
|
|
2044
1687
|
|
|
2045
1688
|
def _drain_pending_nodes(self, pid: int):
|
|
2046
1689
|
"""
|
|
2047
|
-
|
|
1690
|
+
Flush queued node payloads
|
|
2048
1691
|
|
|
2049
1692
|
:param pid: context PID
|
|
2050
1693
|
"""
|
|
@@ -2064,9 +1707,7 @@ class Renderer(BaseRenderer):
|
|
|
2064
1707
|
elif not replace and hasattr(br, "node"):
|
|
2065
1708
|
br.node.emit(payload)
|
|
2066
1709
|
except Exception:
|
|
2067
|
-
# If something goes wrong, stop draining to avoid dropping further items.
|
|
2068
1710
|
break
|
|
2069
|
-
# stop/clear fallback timer if any
|
|
2070
1711
|
t = self._pending_timer.pop(pid, None)
|
|
2071
1712
|
if t:
|
|
2072
1713
|
try:
|
|
@@ -2076,11 +1717,11 @@ class Renderer(BaseRenderer):
|
|
|
2076
1717
|
|
|
2077
1718
|
def _queue_node(self, pid: int, payload: str, replace: bool):
|
|
2078
1719
|
"""
|
|
2079
|
-
Queue node payload
|
|
1720
|
+
Queue node payload until bridge is ready (with safe fallback)
|
|
2080
1721
|
|
|
2081
1722
|
:param pid: context PID
|
|
2082
|
-
:param payload:
|
|
2083
|
-
:param replace: True if replace
|
|
1723
|
+
:param payload: payload to queue
|
|
1724
|
+
:param replace: True if replace nodes list
|
|
2084
1725
|
"""
|
|
2085
1726
|
q = self._pending_nodes.setdefault(pid, [])
|
|
2086
1727
|
q.append((replace, payload))
|
|
@@ -2090,7 +1731,6 @@ class Renderer(BaseRenderer):
|
|
|
2090
1731
|
t.setInterval(1200) # ms
|
|
2091
1732
|
|
|
2092
1733
|
def on_timeout(pid=pid):
|
|
2093
|
-
# Still not ready? Fallback: flush queued via runJavaScript once.
|
|
2094
1734
|
node = self.get_output_node_by_pid(pid)
|
|
2095
1735
|
if node:
|
|
2096
1736
|
while self._pending_nodes.get(pid):
|
|
@@ -2118,11 +1758,10 @@ class Renderer(BaseRenderer):
|
|
|
2118
1758
|
|
|
2119
1759
|
def _stream_get(self, pid: int) -> tuple[_AppendBuffer, QTimer]:
|
|
2120
1760
|
"""
|
|
2121
|
-
Get
|
|
2122
|
-
Timer is single-shot and (re)started only when there is pending data.
|
|
1761
|
+
Get/create per-PID append buffer and timer
|
|
2123
1762
|
|
|
2124
1763
|
:param pid: context PID
|
|
2125
|
-
:return: (buffer, timer)
|
|
1764
|
+
:return: (buffer, timer)
|
|
2126
1765
|
"""
|
|
2127
1766
|
buf = self._stream_acc.get(pid)
|
|
2128
1767
|
if buf is None:
|
|
@@ -2136,7 +1775,6 @@ class Renderer(BaseRenderer):
|
|
|
2136
1775
|
t.setInterval(self._stream_interval_ms)
|
|
2137
1776
|
|
|
2138
1777
|
def on_timeout(pid=pid):
|
|
2139
|
-
# Flush pending batch and, if more data arrives later, timer will be restarted by _stream_push()
|
|
2140
1778
|
self._stream_flush(pid, force=False)
|
|
2141
1779
|
|
|
2142
1780
|
t.timeout.connect(on_timeout)
|
|
@@ -2146,9 +1784,7 @@ class Renderer(BaseRenderer):
|
|
|
2146
1784
|
|
|
2147
1785
|
def _stream_reset(self, pid: Optional[int]):
|
|
2148
1786
|
"""
|
|
2149
|
-
Reset micro-batch
|
|
2150
|
-
|
|
2151
|
-
Clear buffer, stop timer, clear header and last-flush timestamp.
|
|
1787
|
+
Reset micro-batch state for PID
|
|
2152
1788
|
|
|
2153
1789
|
:param pid: context PID
|
|
2154
1790
|
"""
|
|
@@ -2168,56 +1804,42 @@ class Renderer(BaseRenderer):
|
|
|
2168
1804
|
|
|
2169
1805
|
def _stream_push(self, pid: int, header: str, chunk: str):
|
|
2170
1806
|
"""
|
|
2171
|
-
|
|
2172
|
-
If accumulated size crosses thresholds, flush immediately.
|
|
1807
|
+
Push chunk into buffer and schedule flush
|
|
2173
1808
|
|
|
2174
1809
|
:param pid: context PID
|
|
2175
|
-
:param header: header
|
|
2176
|
-
:param chunk: chunk
|
|
1810
|
+
:param header: optional header (first chunk only)
|
|
1811
|
+
:param chunk: chunk to append
|
|
2177
1812
|
"""
|
|
2178
1813
|
if not chunk:
|
|
2179
1814
|
return
|
|
2180
1815
|
|
|
2181
1816
|
buf, timer = self._stream_get(pid)
|
|
2182
|
-
# Remember last known header for this PID
|
|
2183
1817
|
if header and not self._stream_header.get(pid):
|
|
2184
1818
|
self._stream_header[pid] = header
|
|
2185
1819
|
|
|
2186
|
-
# Append chunk cheaply
|
|
2187
1820
|
buf.append(chunk)
|
|
2188
|
-
|
|
2189
|
-
# Emergency backstop: if buffer is getting too large, flush now
|
|
2190
1821
|
pending_size = getattr(buf, "_size", 0)
|
|
2191
1822
|
if pending_size >= self._stream_emergency_bytes:
|
|
2192
1823
|
self._stream_flush(pid, force=True)
|
|
2193
1824
|
return
|
|
2194
|
-
|
|
2195
|
-
# Size-based early flush for responsiveness
|
|
2196
1825
|
if pending_size >= self._stream_max_bytes:
|
|
2197
1826
|
self._stream_flush(pid, force=True)
|
|
2198
1827
|
return
|
|
2199
|
-
|
|
2200
|
-
# Start timer if not active to flush at ~frame rate
|
|
2201
1828
|
if not timer.isActive():
|
|
2202
1829
|
try:
|
|
2203
1830
|
timer.start()
|
|
2204
1831
|
except Exception:
|
|
2205
|
-
# As a fallback, if timer cannot start, flush synchronously
|
|
2206
1832
|
self._stream_flush(pid, force=True)
|
|
2207
1833
|
|
|
2208
1834
|
def _stream_flush(self, pid: int, force: bool = False):
|
|
2209
1835
|
"""
|
|
2210
|
-
Flush buffered chunks
|
|
2211
|
-
If the bridge is not available, fall back to runJavaScript appendStream(name, chunk).
|
|
2212
|
-
|
|
2213
|
-
If no data is pending, do nothing. Stop the timer if running.
|
|
1836
|
+
Flush buffered chunks via QWebChannel (bridge.chunk.emit(name, chunk, type))
|
|
2214
1837
|
|
|
2215
1838
|
:param pid: context PID
|
|
2216
|
-
:param force: True
|
|
1839
|
+
:param force: True if force flush ignoring interval
|
|
2217
1840
|
"""
|
|
2218
1841
|
buf = self._stream_acc.get(pid)
|
|
2219
1842
|
if buf is None or buf.is_empty():
|
|
2220
|
-
# Nothing to send; stop timer if any
|
|
2221
1843
|
t = self._stream_timer.get(pid)
|
|
2222
1844
|
if t and t.isActive():
|
|
2223
1845
|
try:
|
|
@@ -2228,11 +1850,9 @@ class Renderer(BaseRenderer):
|
|
|
2228
1850
|
|
|
2229
1851
|
node = self.get_output_node_by_pid(pid)
|
|
2230
1852
|
if node is None:
|
|
2231
|
-
# Drop buffer if node is gone to avoid leaks
|
|
2232
1853
|
buf.clear()
|
|
2233
1854
|
return
|
|
2234
1855
|
|
|
2235
|
-
# Stop timer for this flush; next push will re-arm it
|
|
2236
1856
|
t = self._stream_timer.get(pid)
|
|
2237
1857
|
if t and t.isActive():
|
|
2238
1858
|
try:
|
|
@@ -2240,22 +1860,18 @@ class Renderer(BaseRenderer):
|
|
|
2240
1860
|
except Exception:
|
|
2241
1861
|
pass
|
|
2242
1862
|
|
|
2243
|
-
# Gather and clear the pending data in one allocation
|
|
2244
1863
|
data = buf.get_and_clear()
|
|
2245
1864
|
name = self._stream_header.get(pid, "") or ""
|
|
2246
1865
|
|
|
2247
1866
|
try:
|
|
2248
1867
|
br = getattr(node.page(), "bridge", None)
|
|
2249
1868
|
if br is not None and hasattr(br, "chunk"):
|
|
2250
|
-
|
|
2251
|
-
br.chunk.emit(name, data)
|
|
1869
|
+
br.chunk.emit(name, data, "text_delta")
|
|
2252
1870
|
else:
|
|
2253
|
-
# Fallback path if bridge not yet connected on this page
|
|
2254
1871
|
node.page().runJavaScript(
|
|
2255
1872
|
f"if (typeof window.appendStream !== 'undefined') appendStream({self.to_json(name)},{self.to_json(data)});"
|
|
2256
1873
|
)
|
|
2257
1874
|
except Exception:
|
|
2258
|
-
# If something goes wrong, attempt a JS fallback once
|
|
2259
1875
|
try:
|
|
2260
1876
|
node.page().runJavaScript(
|
|
2261
1877
|
f"if (typeof window.appendStream !== 'undefined') appendStream({self.to_json(name)},{self.to_json(data)});"
|
|
@@ -2263,16 +1879,13 @@ class Renderer(BaseRenderer):
|
|
|
2263
1879
|
except Exception:
|
|
2264
1880
|
pass
|
|
2265
1881
|
|
|
2266
|
-
# Opportunistic memory release on Linux for very large flushes
|
|
2267
1882
|
try:
|
|
2268
1883
|
if len(data) >= (256 * 1024):
|
|
2269
1884
|
self.auto_cleanup_soft()
|
|
2270
1885
|
except Exception:
|
|
2271
1886
|
pass
|
|
2272
1887
|
|
|
2273
|
-
# Explicitly drop reference to large string so GC/malloc_trim can reclaim sooner
|
|
2274
1888
|
del data
|
|
2275
|
-
|
|
2276
1889
|
self._stream_last_flush[pid] = monotonic()
|
|
2277
1890
|
|
|
2278
1891
|
def eval_js(self, script: str):
|
|
@@ -2292,4 +1905,144 @@ class Renderer(BaseRenderer):
|
|
|
2292
1905
|
try:
|
|
2293
1906
|
node.page().runJavaScript(script, callback)
|
|
2294
1907
|
except Exception:
|
|
2295
|
-
pass
|
|
1908
|
+
pass
|
|
1909
|
+
|
|
1910
|
+
# ------------------------- Helpers: build JSON blocks -------------------------
|
|
1911
|
+
|
|
1912
|
+
def _output_identity(self, ctx: CtxItem) -> Tuple[str, Optional[str], bool]:
|
|
1913
|
+
"""
|
|
1914
|
+
Resolve output identity (name, avatar file:// path) based on preset.
|
|
1915
|
+
|
|
1916
|
+
:param ctx: context item
|
|
1917
|
+
:return: (name, avatar, personalize)
|
|
1918
|
+
"""
|
|
1919
|
+
meta = ctx.meta
|
|
1920
|
+
if meta is None:
|
|
1921
|
+
return self.pids[self.get_or_create_pid(meta)].name_bot if meta else "", None, False
|
|
1922
|
+
preset_id = meta.preset
|
|
1923
|
+
if not preset_id:
|
|
1924
|
+
return self.pids[self.get_or_create_pid(meta)].name_bot, None, False
|
|
1925
|
+
preset = self.window.core.presets.get(preset_id)
|
|
1926
|
+
if preset is None or not preset.ai_personalize:
|
|
1927
|
+
return self.pids[self.get_or_create_pid(meta)].name_bot, None, False
|
|
1928
|
+
name = preset.ai_name or self.pids[self.get_or_create_pid(meta)].name_bot
|
|
1929
|
+
avatar = None
|
|
1930
|
+
if preset.ai_avatar:
|
|
1931
|
+
presets_dir = self.window.core.config.get_user_dir("presets")
|
|
1932
|
+
avatars_dir = os.path.join(presets_dir, "avatars")
|
|
1933
|
+
avatar_path = os.path.join(avatars_dir, preset.ai_avatar)
|
|
1934
|
+
if os.path.exists(avatar_path):
|
|
1935
|
+
avatar = f"{self._file_prefix}{avatar_path}"
|
|
1936
|
+
return name, avatar, bool(preset.ai_personalize)
|
|
1937
|
+
|
|
1938
|
+
def _build_render_block(
|
|
1939
|
+
self,
|
|
1940
|
+
meta: CtxMeta,
|
|
1941
|
+
ctx: CtxItem,
|
|
1942
|
+
input_text: Optional[str],
|
|
1943
|
+
output_text: Optional[str],
|
|
1944
|
+
prev_ctx: Optional[CtxItem] = None,
|
|
1945
|
+
next_ctx: Optional[CtxItem] = None
|
|
1946
|
+
) -> Optional[RenderBlock]:
|
|
1947
|
+
"""
|
|
1948
|
+
Build RenderBlock for given ctx and payloads (input/output).
|
|
1949
|
+
|
|
1950
|
+
- if input_text or output_text is None, that part is skipped.
|
|
1951
|
+
- if ctx.hidden is True, returns None.
|
|
1952
|
+
- if both input_text and output_text are None, returns None.
|
|
1953
|
+
- if meta is None or invalid, returns None.
|
|
1954
|
+
|
|
1955
|
+
:param meta: CtxMeta object
|
|
1956
|
+
:param ctx: CtxItem object
|
|
1957
|
+
:param input_text: Input text (raw, un-formatted)
|
|
1958
|
+
:param prev_ctx: Previous CtxItem (for context, optional)
|
|
1959
|
+
:param next_ctx: Next CtxItem (for context, optional)
|
|
1960
|
+
:return: RenderBlock object or None
|
|
1961
|
+
"""
|
|
1962
|
+
pid = self.get_or_create_pid(meta)
|
|
1963
|
+
if pid is None:
|
|
1964
|
+
return
|
|
1965
|
+
|
|
1966
|
+
block = RenderBlock(id=getattr(ctx, "id", None), meta_id=getattr(meta, "id", None))
|
|
1967
|
+
|
|
1968
|
+
# input
|
|
1969
|
+
if input_text:
|
|
1970
|
+
# Keep raw; formatting is a template duty (escape/BR etc.)
|
|
1971
|
+
block.input = {
|
|
1972
|
+
"type": "user",
|
|
1973
|
+
"name": self.pids[pid].name_user,
|
|
1974
|
+
"avatar_img": None, # no user avatar by default
|
|
1975
|
+
"text": str(input_text),
|
|
1976
|
+
"timestamp": ctx.input_timestamp if hasattr(ctx, "input_timestamp") else None,
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
# output
|
|
1980
|
+
if output_text:
|
|
1981
|
+
# Pre/post format raw markdown via Helpers to preserve placeholders ([!cmd], think) and workdir tokens.
|
|
1982
|
+
md_src = self.helpers.pre_format_text(output_text)
|
|
1983
|
+
md_text = self.helpers.post_format_text(md_src)
|
|
1984
|
+
name, avatar, personalize = self._output_identity(ctx)
|
|
1985
|
+
|
|
1986
|
+
# tool output visibility (agent step / commands)
|
|
1987
|
+
is_cmd = (
|
|
1988
|
+
next_ctx is not None and
|
|
1989
|
+
next_ctx.internal and
|
|
1990
|
+
(len(ctx.cmds) > 0 or (ctx.extra_ctx is not None and len(ctx.extra_ctx) > 0))
|
|
1991
|
+
)
|
|
1992
|
+
tool_output = ""
|
|
1993
|
+
tool_output_visible = False
|
|
1994
|
+
if is_cmd:
|
|
1995
|
+
if ctx.results is not None and len(ctx.results) > 0 \
|
|
1996
|
+
and isinstance(ctx.extra, dict) and "agent_step" in ctx.extra:
|
|
1997
|
+
tool_output = self.helpers.format_cmd_text(str(ctx.input), indent=True)
|
|
1998
|
+
tool_output_visible = True
|
|
1999
|
+
else:
|
|
2000
|
+
tool_output = self.helpers.format_cmd_text(str(next_ctx.input), indent=True)
|
|
2001
|
+
tool_output_visible = True
|
|
2002
|
+
elif ctx.results is not None and len(ctx.results) > 0 \
|
|
2003
|
+
and isinstance(ctx.extra, dict) and "agent_step" in ctx.extra:
|
|
2004
|
+
tool_output = self.helpers.format_cmd_text(str(ctx.input), indent=True)
|
|
2005
|
+
|
|
2006
|
+
# plugin-driven extra (HTML) – keep as-is to preserve functionality
|
|
2007
|
+
tool_extra_html = self.body.prepare_tool_extra(ctx)
|
|
2008
|
+
|
|
2009
|
+
block.output = {
|
|
2010
|
+
"type": "bot",
|
|
2011
|
+
"name": name or self.pids[pid].name_bot,
|
|
2012
|
+
"avatar_img": avatar,
|
|
2013
|
+
"text": md_text,
|
|
2014
|
+
"timestamp": ctx.output_timestamp if hasattr(ctx, "output_timestamp") else None,
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
# extras (images/files/urls/actions)
|
|
2018
|
+
images, files, urls, extra_actions = self.body.build_extras_dicts(ctx, pid)
|
|
2019
|
+
block.images = images
|
|
2020
|
+
block.files = files
|
|
2021
|
+
block.urls = urls
|
|
2022
|
+
|
|
2023
|
+
# docs as raw data -> rendered in JS
|
|
2024
|
+
docs_norm = []
|
|
2025
|
+
if self.window.core.config.get('ctx.sources'):
|
|
2026
|
+
if ctx.doc_ids is not None and len(ctx.doc_ids) > 0:
|
|
2027
|
+
docs_norm = self.body.normalize_docs(ctx.doc_ids)
|
|
2028
|
+
|
|
2029
|
+
block.extra.update({
|
|
2030
|
+
"tool_output": tool_output,
|
|
2031
|
+
"tool_output_visible": tool_output_visible,
|
|
2032
|
+
"tool_extra_html": tool_extra_html,
|
|
2033
|
+
"docs": docs_norm,
|
|
2034
|
+
"footer_icons": True,
|
|
2035
|
+
"personalize": personalize,
|
|
2036
|
+
})
|
|
2037
|
+
block.extra.update(extra_actions)
|
|
2038
|
+
|
|
2039
|
+
# carry ctx.extra flags as-is (do not collide with our own keys)
|
|
2040
|
+
if isinstance(ctx.extra, dict):
|
|
2041
|
+
block.extra.setdefault("ctx_extra", ctx.extra)
|
|
2042
|
+
|
|
2043
|
+
# debug
|
|
2044
|
+
if self.is_debug():
|
|
2045
|
+
print(block.debug())
|
|
2046
|
+
block.extra["debug_html"] = self.append_debug(ctx, pid, "output")
|
|
2047
|
+
|
|
2048
|
+
return block
|