pygpt-net 2.6.44__py3-none-any.whl → 2.6.46__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 +12 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +0 -5
- pygpt_net/controller/ctx/ctx.py +6 -0
- pygpt_net/controller/debug/debug.py +11 -9
- pygpt_net/controller/debug/fixtures.py +1 -1
- pygpt_net/controller/dialogs/debug.py +40 -29
- pygpt_net/core/debug/agent.py +19 -14
- pygpt_net/core/debug/assistants.py +22 -24
- pygpt_net/core/debug/attachments.py +11 -7
- pygpt_net/core/debug/config.py +22 -23
- pygpt_net/core/debug/console/console.py +2 -1
- pygpt_net/core/debug/context.py +63 -63
- pygpt_net/core/debug/db.py +1 -4
- pygpt_net/core/debug/debug.py +1 -1
- pygpt_net/core/debug/events.py +14 -11
- pygpt_net/core/debug/indexes.py +41 -76
- pygpt_net/core/debug/kernel.py +11 -8
- pygpt_net/core/debug/models.py +20 -15
- pygpt_net/core/debug/plugins.py +9 -6
- pygpt_net/core/debug/presets.py +16 -11
- pygpt_net/core/debug/tabs.py +28 -22
- pygpt_net/core/debug/ui.py +25 -22
- pygpt_net/core/fixtures/stream/generator.py +1 -2
- pygpt_net/core/render/web/body.py +290 -23
- pygpt_net/core/render/web/helpers.py +26 -0
- pygpt_net/core/render/web/renderer.py +459 -705
- pygpt_net/core/tabs/tab.py +14 -1
- pygpt_net/data/config/config.json +3 -3
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/settings.json +15 -17
- pygpt_net/data/css/style.dark.css +6 -0
- pygpt_net/data/css/web-blocks.css +4 -0
- pygpt_net/data/css/web-blocks.light.css +1 -1
- pygpt_net/data/css/web-chatgpt.css +4 -0
- pygpt_net/data/css/web-chatgpt.light.css +1 -1
- pygpt_net/data/css/web-chatgpt_wide.css +4 -0
- pygpt_net/data/css/web-chatgpt_wide.light.css +1 -1
- pygpt_net/data/fixtures/fake_stream.txt +5733 -0
- pygpt_net/data/js/app.js +1921 -901
- pygpt_net/data/locale/locale.de.ini +1 -1
- pygpt_net/data/locale/locale.en.ini +5 -5
- pygpt_net/data/locale/locale.es.ini +1 -1
- pygpt_net/data/locale/locale.fr.ini +1 -1
- pygpt_net/data/locale/locale.it.ini +1 -1
- pygpt_net/data/locale/locale.pl.ini +2 -2
- pygpt_net/data/locale/locale.uk.ini +1 -1
- pygpt_net/data/locale/locale.zh.ini +1 -1
- pygpt_net/item/model.py +4 -1
- pygpt_net/js_rc.py +13076 -10198
- pygpt_net/provider/api/anthropic/__init__.py +3 -1
- pygpt_net/provider/api/anthropic/tools.py +1 -1
- pygpt_net/provider/api/google/__init__.py +7 -1
- pygpt_net/provider/api/x_ai/__init__.py +5 -1
- pygpt_net/provider/core/config/patch.py +14 -1
- pygpt_net/provider/llms/anthropic.py +37 -5
- pygpt_net/provider/llms/azure_openai.py +3 -1
- pygpt_net/provider/llms/base.py +13 -1
- pygpt_net/provider/llms/deepseek_api.py +13 -3
- pygpt_net/provider/llms/google.py +14 -1
- pygpt_net/provider/llms/hugging_face_api.py +105 -24
- pygpt_net/provider/llms/hugging_face_embedding.py +88 -0
- pygpt_net/provider/llms/hugging_face_router.py +28 -16
- pygpt_net/provider/llms/local.py +2 -0
- pygpt_net/provider/llms/mistral.py +60 -3
- pygpt_net/provider/llms/open_router.py +4 -2
- pygpt_net/provider/llms/openai.py +4 -1
- pygpt_net/provider/llms/perplexity.py +66 -5
- pygpt_net/provider/llms/utils.py +39 -0
- pygpt_net/provider/llms/voyage.py +50 -0
- pygpt_net/provider/llms/x_ai.py +70 -10
- pygpt_net/ui/layout/chat/output.py +1 -1
- pygpt_net/ui/widget/lists/db.py +1 -0
- pygpt_net/ui/widget/lists/debug.py +1 -0
- pygpt_net/ui/widget/tabs/body.py +12 -1
- pygpt_net/ui/widget/textarea/web.py +4 -4
- pygpt_net/utils.py +3 -2
- {pygpt_net-2.6.44.dist-info → pygpt_net-2.6.46.dist-info}/METADATA +73 -16
- {pygpt_net-2.6.44.dist-info → pygpt_net-2.6.46.dist-info}/RECORD +82 -78
- {pygpt_net-2.6.44.dist-info → pygpt_net-2.6.46.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.44.dist-info → pygpt_net-2.6.46.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.44.dist-info → pygpt_net-2.6.46.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.14 20:00: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,58 @@ 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
|
|
884
|
+
:param pid: context PID
|
|
885
|
+
:param payload: payload to append
|
|
886
|
+
:param flush: True if flush immediately (legacy HTML path)
|
|
887
|
+
:param replace: True if replace whole output (legacy HTML path)
|
|
1015
888
|
"""
|
|
1016
889
|
if self.pids[pid].loaded and not self.pids[pid].use_buffer:
|
|
1017
890
|
self.clear_chunks(pid)
|
|
1018
|
-
if
|
|
1019
|
-
self.flush_output(pid,
|
|
891
|
+
if payload:
|
|
892
|
+
self.flush_output(pid, payload, replace)
|
|
1020
893
|
self.pids[pid].clear()
|
|
1021
894
|
else:
|
|
1022
895
|
if not flush:
|
|
1023
|
-
self.pids[pid].append_html(
|
|
1024
|
-
html = None # free reference
|
|
896
|
+
self.pids[pid].append_html(payload)
|
|
1025
897
|
|
|
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
|
-
):
|
|
898
|
+
def append_context_item(self, meta: CtxMeta, ctx: CtxItem,
|
|
899
|
+
prev_ctx: Optional[CtxItem] = None, next_ctx: Optional[CtxItem] = None):
|
|
1033
900
|
"""
|
|
1034
|
-
Append context item
|
|
901
|
+
Append context item as one RenderBlock with input+output (if present)
|
|
1035
902
|
|
|
1036
903
|
:param meta: context meta
|
|
1037
904
|
:param ctx: context item
|
|
1038
905
|
:param prev_ctx: previous context item
|
|
1039
906
|
:param next_ctx: next context item
|
|
1040
907
|
"""
|
|
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:
|
|
908
|
+
input_text = self.prepare_input(meta, ctx, flush=False, append=False)
|
|
909
|
+
output_text = self.prepare_output(meta, ctx, flush=False, prev_ctx=prev_ctx, next_ctx=next_ctx)
|
|
910
|
+
block = self._build_render_block(meta, ctx, input_text, output_text, prev_ctx=prev_ctx, next_ctx=next_ctx)
|
|
911
|
+
if block:
|
|
912
|
+
pid = self.get_or_create_pid(meta)
|
|
913
|
+
self.append(pid, block.to_json(wrap=True))
|
|
914
|
+
|
|
915
|
+
def append_extra(self, meta: CtxMeta, ctx: CtxItem, footer: bool = False, render: bool = True) -> str:
|
|
1061
916
|
"""
|
|
1062
|
-
Append extra data (
|
|
917
|
+
Append extra data (legacy HTML way) – kept for runtime calls that append later.
|
|
1063
918
|
|
|
1064
|
-
:param meta:
|
|
919
|
+
:param meta: context meta
|
|
1065
920
|
:param ctx: context item
|
|
1066
|
-
:param footer: True if
|
|
1067
|
-
:param render: True if render
|
|
1068
|
-
:return: HTML
|
|
921
|
+
:param footer: True if append at the end (legacy)
|
|
922
|
+
:param render: True if render to output (legacy)
|
|
923
|
+
:return: rendered HTML
|
|
1069
924
|
"""
|
|
1070
925
|
self.tool_output_end()
|
|
1071
926
|
|
|
@@ -1086,7 +941,7 @@ class Renderer(BaseRenderer):
|
|
|
1086
941
|
html_parts.append(self.body.get_image_html(image, n, c))
|
|
1087
942
|
self.pids[pid].images_appended.append(image)
|
|
1088
943
|
n += 1
|
|
1089
|
-
except Exception
|
|
944
|
+
except Exception:
|
|
1090
945
|
pass
|
|
1091
946
|
|
|
1092
947
|
c = len(ctx.files)
|
|
@@ -1101,7 +956,7 @@ class Renderer(BaseRenderer):
|
|
|
1101
956
|
files_html.append(self.body.get_file_html(file, n, c))
|
|
1102
957
|
self.pids[pid].files_appended.append(file)
|
|
1103
958
|
n += 1
|
|
1104
|
-
except Exception
|
|
959
|
+
except Exception:
|
|
1105
960
|
pass
|
|
1106
961
|
if files_html:
|
|
1107
962
|
html_parts.append("<br/><br/>".join(files_html))
|
|
@@ -1118,7 +973,7 @@ class Renderer(BaseRenderer):
|
|
|
1118
973
|
urls_html.append(self.body.get_url_html(url, n, c))
|
|
1119
974
|
self.pids[pid].urls_appended.append(url)
|
|
1120
975
|
n += 1
|
|
1121
|
-
except Exception
|
|
976
|
+
except Exception:
|
|
1122
977
|
pass
|
|
1123
978
|
if urls_html:
|
|
1124
979
|
html_parts.append("<br/><br/>".join(urls_html))
|
|
@@ -1128,7 +983,7 @@ class Renderer(BaseRenderer):
|
|
|
1128
983
|
try:
|
|
1129
984
|
docs = self.body.get_docs_html(ctx.doc_ids)
|
|
1130
985
|
html_parts.append(docs)
|
|
1131
|
-
except Exception
|
|
986
|
+
except Exception:
|
|
1132
987
|
pass
|
|
1133
988
|
|
|
1134
989
|
html = "".join(html_parts)
|
|
@@ -1142,24 +997,19 @@ class Renderer(BaseRenderer):
|
|
|
1142
997
|
self.sanitize_html(html)
|
|
1143
998
|
)});"""
|
|
1144
999
|
)
|
|
1145
|
-
except Exception
|
|
1000
|
+
except Exception:
|
|
1146
1001
|
pass
|
|
1147
1002
|
|
|
1148
1003
|
return html
|
|
1149
1004
|
|
|
1150
|
-
def append_timestamp(
|
|
1151
|
-
self,
|
|
1152
|
-
ctx: CtxItem,
|
|
1153
|
-
text: str,
|
|
1154
|
-
type: Optional[int] = None
|
|
1155
|
-
) -> str:
|
|
1005
|
+
def append_timestamp(self, ctx: CtxItem, text: str, type: Optional[int] = None) -> str:
|
|
1156
1006
|
"""
|
|
1157
|
-
Append timestamp to text
|
|
1007
|
+
Append timestamp to text (legacy HTML path)
|
|
1158
1008
|
|
|
1159
1009
|
:param ctx: context item
|
|
1160
|
-
:param text:
|
|
1161
|
-
:param type:
|
|
1162
|
-
:return:
|
|
1010
|
+
:param text: text to append timestamp to
|
|
1011
|
+
:param type: NODE_INPUT or NODE_OUTPUT
|
|
1012
|
+
:return: text with timestamp
|
|
1163
1013
|
"""
|
|
1164
1014
|
if ctx is not None and ctx.input_timestamp is not None:
|
|
1165
1015
|
timestamp = None
|
|
@@ -1173,16 +1023,12 @@ class Renderer(BaseRenderer):
|
|
|
1173
1023
|
text = f'<span class="ts">{hour}: </span>{text}'
|
|
1174
1024
|
return text
|
|
1175
1025
|
|
|
1176
|
-
def reset(
|
|
1177
|
-
self,
|
|
1178
|
-
meta: Optional[CtxMeta] = None,
|
|
1179
|
-
clear_nodes: bool = True
|
|
1180
|
-
):
|
|
1026
|
+
def reset(self, meta: Optional[CtxMeta] = None, clear_nodes: bool = True):
|
|
1181
1027
|
"""
|
|
1182
|
-
Reset
|
|
1028
|
+
Reset current output
|
|
1183
1029
|
|
|
1184
|
-
:param meta:
|
|
1185
|
-
:param clear_nodes: True if clear nodes
|
|
1030
|
+
:param meta: context meta
|
|
1031
|
+
:param clear_nodes: True if clear nodes list
|
|
1186
1032
|
"""
|
|
1187
1033
|
pid = self.get_pid(meta)
|
|
1188
1034
|
if pid is not None and pid in self.pids:
|
|
@@ -1191,7 +1037,6 @@ class Renderer(BaseRenderer):
|
|
|
1191
1037
|
if meta is not None:
|
|
1192
1038
|
pid = self.get_or_create_pid(meta)
|
|
1193
1039
|
self.reset_by_pid(pid, clear_nodes=clear_nodes)
|
|
1194
|
-
|
|
1195
1040
|
self.clear_live(meta, CtxItem())
|
|
1196
1041
|
|
|
1197
1042
|
def reset_by_pid(self, pid: Optional[int], clear_nodes: bool = True):
|
|
@@ -1199,7 +1044,7 @@ class Renderer(BaseRenderer):
|
|
|
1199
1044
|
Reset by PID
|
|
1200
1045
|
|
|
1201
1046
|
:param pid: context PID
|
|
1202
|
-
:param clear_nodes: True if clear nodes
|
|
1047
|
+
:param clear_nodes: True if clear nodes list
|
|
1203
1048
|
"""
|
|
1204
1049
|
self.pids[pid].item = None
|
|
1205
1050
|
self.pids[pid].html = ""
|
|
@@ -1214,27 +1059,24 @@ class Renderer(BaseRenderer):
|
|
|
1214
1059
|
node.reset_current_content()
|
|
1215
1060
|
self.reset_names_by_pid(pid)
|
|
1216
1061
|
self.prev_chunk_replace = False
|
|
1217
|
-
self._stream_reset(pid)
|
|
1062
|
+
self._stream_reset(pid)
|
|
1218
1063
|
|
|
1219
1064
|
def clear_input(self):
|
|
1220
1065
|
"""Clear input"""
|
|
1221
1066
|
self.get_input_node().clear()
|
|
1222
1067
|
|
|
1223
|
-
def clear_output(
|
|
1224
|
-
self,
|
|
1225
|
-
meta: Optional[CtxMeta] = None
|
|
1226
|
-
):
|
|
1068
|
+
def clear_output(self, meta: Optional[CtxMeta] = None):
|
|
1227
1069
|
"""
|
|
1228
1070
|
Clear output
|
|
1229
1071
|
|
|
1230
|
-
:param meta:
|
|
1072
|
+
:param meta: context meta
|
|
1231
1073
|
"""
|
|
1232
1074
|
self.prev_chunk_replace = False
|
|
1233
1075
|
self.reset(meta)
|
|
1234
1076
|
|
|
1235
|
-
def clear_chunks(self, pid):
|
|
1077
|
+
def clear_chunks(self, pid: Optional[int]):
|
|
1236
1078
|
"""
|
|
1237
|
-
Clear chunks
|
|
1079
|
+
Clear current chunks
|
|
1238
1080
|
|
|
1239
1081
|
:param pid: context PID
|
|
1240
1082
|
"""
|
|
@@ -1243,14 +1085,11 @@ class Renderer(BaseRenderer):
|
|
|
1243
1085
|
self.clear_chunks_input(pid)
|
|
1244
1086
|
self.clear_chunks_output(pid)
|
|
1245
1087
|
|
|
1246
|
-
def clear_chunks_input(
|
|
1247
|
-
self,
|
|
1248
|
-
pid: Optional[int]
|
|
1249
|
-
):
|
|
1088
|
+
def clear_chunks_input(self, pid: Optional[int]):
|
|
1250
1089
|
"""
|
|
1251
1090
|
Clear chunks from input
|
|
1252
1091
|
|
|
1253
|
-
:pid: context PID
|
|
1092
|
+
:param pid: context PID
|
|
1254
1093
|
"""
|
|
1255
1094
|
if pid is None:
|
|
1256
1095
|
return
|
|
@@ -1261,14 +1100,11 @@ class Renderer(BaseRenderer):
|
|
|
1261
1100
|
except Exception:
|
|
1262
1101
|
pass
|
|
1263
1102
|
|
|
1264
|
-
def clear_chunks_output(
|
|
1265
|
-
self,
|
|
1266
|
-
pid: Optional[int]
|
|
1267
|
-
):
|
|
1103
|
+
def clear_chunks_output(self, pid: Optional[int]):
|
|
1268
1104
|
"""
|
|
1269
1105
|
Clear chunks from output
|
|
1270
1106
|
|
|
1271
|
-
:pid: context PID
|
|
1107
|
+
:param pid: context PID
|
|
1272
1108
|
"""
|
|
1273
1109
|
self.prev_chunk_replace = False
|
|
1274
1110
|
try:
|
|
@@ -1277,16 +1113,13 @@ class Renderer(BaseRenderer):
|
|
|
1277
1113
|
)
|
|
1278
1114
|
except Exception:
|
|
1279
1115
|
pass
|
|
1280
|
-
self._stream_reset(pid)
|
|
1116
|
+
self._stream_reset(pid)
|
|
1281
1117
|
|
|
1282
|
-
def clear_nodes(
|
|
1283
|
-
self,
|
|
1284
|
-
pid: Optional[int]
|
|
1285
|
-
):
|
|
1118
|
+
def clear_nodes(self, pid: Optional[int]):
|
|
1286
1119
|
"""
|
|
1287
|
-
Clear nodes
|
|
1120
|
+
Clear nodes list
|
|
1288
1121
|
|
|
1289
|
-
:pid: context PID
|
|
1122
|
+
:param pid: context PID
|
|
1290
1123
|
"""
|
|
1291
1124
|
try:
|
|
1292
1125
|
self.get_output_node_by_pid(pid).page().runJavaScript(
|
|
@@ -1295,173 +1128,33 @@ class Renderer(BaseRenderer):
|
|
|
1295
1128
|
except Exception:
|
|
1296
1129
|
pass
|
|
1297
1130
|
|
|
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:
|
|
1131
|
+
# Legacy methods kept for compatibility with existing code paths
|
|
1132
|
+
def prepare_node(self, meta: CtxMeta, ctx: CtxItem, html: str, type: int = 1,
|
|
1133
|
+
prev_ctx: Optional[CtxItem] = None, next_ctx: Optional[CtxItem] = None) -> str:
|
|
1307
1134
|
"""
|
|
1308
|
-
|
|
1135
|
+
Compatibility shim: convert single input/output into markdown/raw for JS templates.
|
|
1309
1136
|
|
|
1310
1137
|
:param meta: context meta
|
|
1311
|
-
:param ctx:
|
|
1312
|
-
:param html:
|
|
1313
|
-
:param type:
|
|
1138
|
+
:param ctx: context item
|
|
1139
|
+
:param html: HTML content
|
|
1140
|
+
:param type: NODE_INPUT or NODE_OUTPUT
|
|
1314
1141
|
:param prev_ctx: previous context item
|
|
1315
1142
|
:param next_ctx: next context item
|
|
1316
|
-
:return: prepared
|
|
1143
|
+
:return: prepared text
|
|
1317
1144
|
"""
|
|
1318
1145
|
pid = self.get_or_create_pid(meta)
|
|
1319
1146
|
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
|
-
)
|
|
1147
|
+
return str(html)
|
|
1327
1148
|
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
|
-
)
|
|
1149
|
+
return str(html)
|
|
1457
1150
|
|
|
1458
1151
|
def get_name_header(self, ctx: CtxItem, stream: bool = False) -> str:
|
|
1459
1152
|
"""
|
|
1460
|
-
|
|
1153
|
+
Legacy - kept for stream header text (avatar + name string)
|
|
1461
1154
|
|
|
1462
|
-
:param ctx:
|
|
1463
|
-
:param stream: True if
|
|
1464
|
-
:return: HTML
|
|
1155
|
+
:param ctx: context item
|
|
1156
|
+
:param stream: True if streaming mode
|
|
1157
|
+
:return: HTML string
|
|
1465
1158
|
"""
|
|
1466
1159
|
meta = ctx.meta
|
|
1467
1160
|
if meta is None:
|
|
@@ -1494,13 +1187,13 @@ class Renderer(BaseRenderer):
|
|
|
1494
1187
|
else:
|
|
1495
1188
|
return f"<div class=\"name-header name-bot\">{avatar_html}{output_name}</div>"
|
|
1496
1189
|
|
|
1497
|
-
def flush_output(self, pid: int,
|
|
1190
|
+
def flush_output(self, pid: int, payload: str, replace: bool = False):
|
|
1498
1191
|
"""
|
|
1499
|
-
Send content via QWebChannel
|
|
1192
|
+
Send content via QWebChannel (JSON or HTML string).
|
|
1500
1193
|
|
|
1501
1194
|
:param pid: context PID
|
|
1502
|
-
:param
|
|
1503
|
-
:param replace: True if replace
|
|
1195
|
+
:param payload: payload to send
|
|
1196
|
+
:param replace: True if replace nodes list
|
|
1504
1197
|
"""
|
|
1505
1198
|
if pid is None:
|
|
1506
1199
|
return
|
|
@@ -1516,23 +1209,23 @@ class Renderer(BaseRenderer):
|
|
|
1516
1209
|
if br is not None:
|
|
1517
1210
|
if replace and hasattr(br, "nodeReplace"):
|
|
1518
1211
|
self.clear_nodes(pid)
|
|
1519
|
-
br.nodeReplace.emit(
|
|
1212
|
+
br.nodeReplace.emit(payload)
|
|
1520
1213
|
return
|
|
1521
1214
|
if not replace and hasattr(br, "node"):
|
|
1522
|
-
br.node.emit(
|
|
1215
|
+
br.node.emit(payload)
|
|
1523
1216
|
return
|
|
1524
|
-
# Not ready
|
|
1525
|
-
self._queue_node(pid,
|
|
1217
|
+
# Not ready -> queue
|
|
1218
|
+
self._queue_node(pid, payload, replace)
|
|
1526
1219
|
except Exception:
|
|
1527
|
-
#
|
|
1220
|
+
# Fallback to runJavaScript path
|
|
1528
1221
|
try:
|
|
1529
1222
|
if replace:
|
|
1530
1223
|
node.page().runJavaScript(
|
|
1531
|
-
f"if (typeof window.replaceNodes !== 'undefined') replaceNodes({self.to_json(
|
|
1224
|
+
f"if (typeof window.replaceNodes !== 'undefined') replaceNodes({self.to_json(payload)});"
|
|
1532
1225
|
)
|
|
1533
1226
|
else:
|
|
1534
1227
|
node.page().runJavaScript(
|
|
1535
|
-
f"if (typeof window.appendNode !== 'undefined') appendNode({self.to_json(
|
|
1228
|
+
f"if (typeof window.appendNode !== 'undefined') appendNode({self.to_json(payload)});"
|
|
1536
1229
|
)
|
|
1537
1230
|
except Exception:
|
|
1538
1231
|
pass
|
|
@@ -1541,13 +1234,9 @@ class Renderer(BaseRenderer):
|
|
|
1541
1234
|
"""Reload output, called externally only on theme change to redraw content"""
|
|
1542
1235
|
self.window.controller.ctx.refresh_output()
|
|
1543
1236
|
|
|
1544
|
-
def flush(
|
|
1545
|
-
self,
|
|
1546
|
-
pid:
|
|
1547
|
-
Optional[int]
|
|
1548
|
-
):
|
|
1237
|
+
def flush(self, pid: Optional[int]):
|
|
1549
1238
|
"""
|
|
1550
|
-
Flush output
|
|
1239
|
+
Flush output page HTML (initial)
|
|
1551
1240
|
|
|
1552
1241
|
:param pid: context PID
|
|
1553
1242
|
"""
|
|
@@ -1559,20 +1248,16 @@ class Renderer(BaseRenderer):
|
|
|
1559
1248
|
if node is not None:
|
|
1560
1249
|
node.setHtml(html, baseUrl="file://")
|
|
1561
1250
|
|
|
1562
|
-
def fresh(
|
|
1563
|
-
self,
|
|
1564
|
-
meta: Optional[CtxMeta] = None,
|
|
1565
|
-
force: bool = False
|
|
1566
|
-
):
|
|
1251
|
+
def fresh(self, meta: Optional[CtxMeta] = None, force: bool = False):
|
|
1567
1252
|
"""
|
|
1568
1253
|
Reset page / unload old renderer from memory
|
|
1569
1254
|
|
|
1570
1255
|
:param meta: context meta
|
|
1571
|
-
:param force: True if force
|
|
1256
|
+
:param force: True if force even when not needed
|
|
1572
1257
|
"""
|
|
1573
1258
|
plain = self.window.core.config.get('render.plain')
|
|
1574
1259
|
if plain:
|
|
1575
|
-
return
|
|
1260
|
+
return
|
|
1576
1261
|
|
|
1577
1262
|
pid = self.get_or_create_pid(meta)
|
|
1578
1263
|
if pid is None:
|
|
@@ -1599,26 +1284,20 @@ class Renderer(BaseRenderer):
|
|
|
1599
1284
|
self.recycle(node, meta)
|
|
1600
1285
|
self.flush(pid)
|
|
1601
1286
|
|
|
1602
|
-
def recycle(
|
|
1603
|
-
self,
|
|
1604
|
-
node: ChatWebOutput,
|
|
1605
|
-
meta: Optional[CtxMeta] = None
|
|
1606
|
-
):
|
|
1287
|
+
def recycle(self, node: ChatWebOutput, meta: Optional[CtxMeta] = None):
|
|
1607
1288
|
"""
|
|
1608
|
-
Recycle renderer to
|
|
1609
|
-
|
|
1610
|
-
Swaps out the old QWebEngineView with a fresh instance.
|
|
1289
|
+
Recycle renderer to avoid leaks
|
|
1611
1290
|
|
|
1612
|
-
:param node: output node
|
|
1291
|
+
:param node: output node to recycle
|
|
1613
1292
|
:param meta: context meta
|
|
1614
1293
|
"""
|
|
1615
1294
|
tab = node.get_tab()
|
|
1616
|
-
|
|
1295
|
+
tab.delete_ref(node)
|
|
1296
|
+
layout = tab.child.layout()
|
|
1617
1297
|
layout.removeWidget(node)
|
|
1618
1298
|
self.window.ui.nodes['output'].pop(tab.pid, None)
|
|
1619
|
-
tab.child.delete_refs()
|
|
1620
1299
|
|
|
1621
|
-
node.on_delete()
|
|
1300
|
+
node.on_delete()
|
|
1622
1301
|
|
|
1623
1302
|
view = ChatWebOutput(self.window)
|
|
1624
1303
|
view.set_tab(tab)
|
|
@@ -1633,31 +1312,25 @@ class Renderer(BaseRenderer):
|
|
|
1633
1312
|
gc.collect()
|
|
1634
1313
|
except Exception:
|
|
1635
1314
|
pass
|
|
1636
|
-
self.auto_cleanup_soft(meta)
|
|
1315
|
+
self.auto_cleanup_soft(meta)
|
|
1637
1316
|
|
|
1638
|
-
def get_output_node(
|
|
1639
|
-
self,
|
|
1640
|
-
meta: Optional[CtxMeta] = None
|
|
1641
|
-
) -> Optional[ChatWebOutput]:
|
|
1317
|
+
def get_output_node(self, meta: Optional[CtxMeta] = None) -> Optional[ChatWebOutput]:
|
|
1642
1318
|
"""
|
|
1643
1319
|
Get output node
|
|
1644
1320
|
|
|
1645
1321
|
:param meta: context meta
|
|
1646
|
-
:return: output node
|
|
1322
|
+
:return: output node or None
|
|
1647
1323
|
"""
|
|
1648
1324
|
if self._get_output_node_by_meta is None:
|
|
1649
1325
|
self._get_output_node_by_meta = self.window.core.ctx.output.get_output_node_by_meta
|
|
1650
1326
|
return self._get_output_node_by_meta(meta)
|
|
1651
1327
|
|
|
1652
|
-
def get_output_node_by_pid(
|
|
1653
|
-
self,
|
|
1654
|
-
pid: Optional[int]
|
|
1655
|
-
) -> Optional[ChatWebOutput]:
|
|
1328
|
+
def get_output_node_by_pid(self, pid: Optional[int]) -> Optional[ChatWebOutput]:
|
|
1656
1329
|
"""
|
|
1657
1330
|
Get output node by PID
|
|
1658
1331
|
|
|
1659
|
-
:param pid: context
|
|
1660
|
-
:return: output node
|
|
1332
|
+
:param pid: context PID
|
|
1333
|
+
:return: output node or None
|
|
1661
1334
|
"""
|
|
1662
1335
|
if self._get_output_node_by_pid is None:
|
|
1663
1336
|
self._get_output_node_by_pid = self.window.core.ctx.output.get_output_node_by_pid
|
|
@@ -1701,7 +1374,7 @@ class Renderer(BaseRenderer):
|
|
|
1701
1374
|
"""
|
|
1702
1375
|
Reset names
|
|
1703
1376
|
|
|
1704
|
-
|
|
1377
|
+
:param meta: context meta
|
|
1705
1378
|
"""
|
|
1706
1379
|
if meta is None:
|
|
1707
1380
|
return
|
|
@@ -1712,7 +1385,7 @@ class Renderer(BaseRenderer):
|
|
|
1712
1385
|
"""
|
|
1713
1386
|
Reset names by PID
|
|
1714
1387
|
|
|
1715
|
-
:param pid: context
|
|
1388
|
+
:param pid: context PID
|
|
1716
1389
|
"""
|
|
1717
1390
|
self.pids[pid].name_user = trans("chat.name.user")
|
|
1718
1391
|
self.pids[pid].name_bot = trans("chat.name.bot")
|
|
@@ -1727,7 +1400,7 @@ class Renderer(BaseRenderer):
|
|
|
1727
1400
|
|
|
1728
1401
|
def on_edit_submit(self, ctx: CtxItem):
|
|
1729
1402
|
"""
|
|
1730
|
-
On
|
|
1403
|
+
On edit submit
|
|
1731
1404
|
|
|
1732
1405
|
:param ctx: context item
|
|
1733
1406
|
"""
|
|
@@ -1737,7 +1410,7 @@ class Renderer(BaseRenderer):
|
|
|
1737
1410
|
"""
|
|
1738
1411
|
On enable edit icons
|
|
1739
1412
|
|
|
1740
|
-
:param live: True if live
|
|
1413
|
+
:param live: True if live mode
|
|
1741
1414
|
"""
|
|
1742
1415
|
if not live:
|
|
1743
1416
|
return
|
|
@@ -1752,7 +1425,7 @@ class Renderer(BaseRenderer):
|
|
|
1752
1425
|
"""
|
|
1753
1426
|
On disable edit icons
|
|
1754
1427
|
|
|
1755
|
-
:param live: True if live
|
|
1428
|
+
:param live: True if live mode
|
|
1756
1429
|
"""
|
|
1757
1430
|
if not live:
|
|
1758
1431
|
return
|
|
@@ -1767,7 +1440,7 @@ class Renderer(BaseRenderer):
|
|
|
1767
1440
|
"""
|
|
1768
1441
|
On enable timestamp
|
|
1769
1442
|
|
|
1770
|
-
:param live: True if live
|
|
1443
|
+
:param live: True if live mode
|
|
1771
1444
|
"""
|
|
1772
1445
|
if not live:
|
|
1773
1446
|
return
|
|
@@ -1782,7 +1455,7 @@ class Renderer(BaseRenderer):
|
|
|
1782
1455
|
"""
|
|
1783
1456
|
On disable timestamp
|
|
1784
1457
|
|
|
1785
|
-
:param live: True if live
|
|
1458
|
+
:param live: True if live mode
|
|
1786
1459
|
"""
|
|
1787
1460
|
if not live:
|
|
1788
1461
|
return
|
|
@@ -1793,13 +1466,9 @@ class Renderer(BaseRenderer):
|
|
|
1793
1466
|
except Exception:
|
|
1794
1467
|
pass
|
|
1795
1468
|
|
|
1796
|
-
def update_names(
|
|
1797
|
-
self,
|
|
1798
|
-
meta: CtxMeta,
|
|
1799
|
-
ctx: CtxItem
|
|
1800
|
-
):
|
|
1469
|
+
def update_names(self, meta: CtxMeta, ctx: CtxItem):
|
|
1801
1470
|
"""
|
|
1802
|
-
Update names
|
|
1471
|
+
Update names from ctx
|
|
1803
1472
|
|
|
1804
1473
|
:param meta: context meta
|
|
1805
1474
|
:param ctx: context item
|
|
@@ -1813,7 +1482,7 @@ class Renderer(BaseRenderer):
|
|
|
1813
1482
|
self.pids[pid].name_bot = ctx.output_name
|
|
1814
1483
|
|
|
1815
1484
|
def clear_all(self):
|
|
1816
|
-
"""Clear all"""
|
|
1485
|
+
"""Clear all tabs"""
|
|
1817
1486
|
for pid in self.pids:
|
|
1818
1487
|
self.clear_chunks(pid)
|
|
1819
1488
|
self.clear_nodes(pid)
|
|
@@ -1821,33 +1490,23 @@ class Renderer(BaseRenderer):
|
|
|
1821
1490
|
self._stream_reset(pid)
|
|
1822
1491
|
|
|
1823
1492
|
def scroll_to_bottom(self):
|
|
1824
|
-
"""Scroll to bottom"""
|
|
1493
|
+
"""Scroll to bottom placeholder"""
|
|
1825
1494
|
pass
|
|
1826
1495
|
|
|
1827
1496
|
def append_block(self):
|
|
1828
|
-
"""Append block
|
|
1497
|
+
"""Append block placeholder"""
|
|
1829
1498
|
pass
|
|
1830
1499
|
|
|
1831
1500
|
def to_end(self, ctx: CtxItem):
|
|
1832
|
-
"""
|
|
1833
|
-
Move cursor to end of output
|
|
1834
|
-
|
|
1835
|
-
:param ctx: context item
|
|
1836
|
-
"""
|
|
1501
|
+
"""Move cursor to end placeholder"""
|
|
1837
1502
|
pass
|
|
1838
1503
|
|
|
1839
1504
|
def get_all_nodes(self) -> list:
|
|
1840
|
-
"""
|
|
1841
|
-
Return all registered nodes
|
|
1842
|
-
|
|
1843
|
-
:return: list of ChatOutput nodes (tabs)
|
|
1844
|
-
"""
|
|
1505
|
+
"""Return all registered nodes"""
|
|
1845
1506
|
return self.window.core.ctx.output.get_all()
|
|
1846
1507
|
|
|
1847
|
-
# TODO: on lang change
|
|
1848
|
-
|
|
1849
1508
|
def reload_css(self):
|
|
1850
|
-
"""Reload CSS
|
|
1509
|
+
"""Reload CSS – propagate theme and config to runtime"""
|
|
1851
1510
|
to_json = self.to_json(self.body.prepare_styles())
|
|
1852
1511
|
nodes = self.get_all_nodes()
|
|
1853
1512
|
for pid in self.pids:
|
|
@@ -1865,7 +1524,7 @@ class Renderer(BaseRenderer):
|
|
|
1865
1524
|
node.page().runJavaScript(
|
|
1866
1525
|
"if (typeof window.disableBlocks !== 'undefined') disableBlocks();"
|
|
1867
1526
|
)
|
|
1868
|
-
except Exception
|
|
1527
|
+
except Exception:
|
|
1869
1528
|
pass
|
|
1870
1529
|
return
|
|
1871
1530
|
|
|
@@ -1877,16 +1536,12 @@ class Renderer(BaseRenderer):
|
|
|
1877
1536
|
self.reload_css()
|
|
1878
1537
|
return
|
|
1879
1538
|
|
|
1880
|
-
def tool_output_append(
|
|
1881
|
-
self,
|
|
1882
|
-
meta: CtxMeta,
|
|
1883
|
-
content: str
|
|
1884
|
-
):
|
|
1539
|
+
def tool_output_append(self, meta: CtxMeta, content: str):
|
|
1885
1540
|
"""
|
|
1886
1541
|
Add tool output (append)
|
|
1887
1542
|
|
|
1888
1543
|
:param meta: context meta
|
|
1889
|
-
:param content: content
|
|
1544
|
+
:param content: content to append
|
|
1890
1545
|
"""
|
|
1891
1546
|
try:
|
|
1892
1547
|
self.get_output_node(meta).page().runJavaScript(
|
|
@@ -1897,16 +1552,12 @@ class Renderer(BaseRenderer):
|
|
|
1897
1552
|
except Exception:
|
|
1898
1553
|
pass
|
|
1899
1554
|
|
|
1900
|
-
def tool_output_update(
|
|
1901
|
-
self,
|
|
1902
|
-
meta: CtxMeta,
|
|
1903
|
-
content: str
|
|
1904
|
-
):
|
|
1555
|
+
def tool_output_update(self, meta: CtxMeta, content: str):
|
|
1905
1556
|
"""
|
|
1906
1557
|
Replace tool output
|
|
1907
1558
|
|
|
1908
1559
|
:param meta: context meta
|
|
1909
|
-
:param content: content
|
|
1560
|
+
:param content: content to set
|
|
1910
1561
|
"""
|
|
1911
1562
|
try:
|
|
1912
1563
|
self.get_output_node(meta).page().runJavaScript(
|
|
@@ -1954,10 +1605,9 @@ class Renderer(BaseRenderer):
|
|
|
1954
1605
|
|
|
1955
1606
|
def sanitize_html(self, html: str) -> str:
|
|
1956
1607
|
"""
|
|
1957
|
-
Sanitize
|
|
1608
|
+
Sanitize HTM
|
|
1958
1609
|
|
|
1959
|
-
:param html: HTML string
|
|
1960
|
-
:return: sanitized HTML string
|
|
1610
|
+
:param html: HTML string
|
|
1961
1611
|
"""
|
|
1962
1612
|
return html
|
|
1963
1613
|
if not html:
|
|
@@ -1966,35 +1616,30 @@ class Renderer(BaseRenderer):
|
|
|
1966
1616
|
return html
|
|
1967
1617
|
return self.RE_AMP_LT_GT.sub(r'&\1;', html)
|
|
1968
1618
|
|
|
1969
|
-
def append_debug(
|
|
1970
|
-
self,
|
|
1971
|
-
ctx: CtxItem,
|
|
1972
|
-
pid,
|
|
1973
|
-
title: Optional[str] = None
|
|
1974
|
-
) -> str:
|
|
1619
|
+
def append_debug(self, ctx: CtxItem, pid, title: Optional[str] = None) -> str:
|
|
1975
1620
|
"""
|
|
1976
|
-
Append debug info
|
|
1621
|
+
Append debug info HTML (legacy path)
|
|
1977
1622
|
|
|
1978
1623
|
:param ctx: context item
|
|
1979
1624
|
:param pid: context PID
|
|
1980
|
-
:param title:
|
|
1981
|
-
:return: HTML
|
|
1625
|
+
:param title: optional title
|
|
1626
|
+
:return: HTML string
|
|
1982
1627
|
"""
|
|
1983
1628
|
if title is None:
|
|
1984
1629
|
title = "debug"
|
|
1985
|
-
return f"<div class='debug'><b>{title}:</b> pid: {pid}, ctx: {ctx.
|
|
1630
|
+
return f"<div class='debug'><b>{title}:</b> pid: {pid}, ctx: {_html.escape(ctx.to_debug())}</div>"
|
|
1986
1631
|
|
|
1987
1632
|
def is_debug(self) -> bool:
|
|
1988
1633
|
"""
|
|
1989
|
-
Check
|
|
1634
|
+
Check debug flag
|
|
1990
1635
|
|
|
1991
|
-
:return: True if debug
|
|
1636
|
+
:return: True if debug enabled
|
|
1992
1637
|
"""
|
|
1993
1638
|
return self.window.core.config.get("debug.render", False)
|
|
1994
1639
|
|
|
1995
1640
|
def js_stream_queue_len(self, pid: int):
|
|
1996
1641
|
"""
|
|
1997
|
-
Ask
|
|
1642
|
+
Ask JS side for stream queue length
|
|
1998
1643
|
|
|
1999
1644
|
:param pid: context PID
|
|
2000
1645
|
"""
|
|
@@ -2012,13 +1657,12 @@ class Renderer(BaseRenderer):
|
|
|
2012
1657
|
|
|
2013
1658
|
def remove_pid(self, pid: int):
|
|
2014
1659
|
"""
|
|
2015
|
-
Remove PID
|
|
1660
|
+
Remove PID and clean resources
|
|
2016
1661
|
|
|
2017
1662
|
:param pid: context PID
|
|
2018
1663
|
"""
|
|
2019
1664
|
if pid in self.pids:
|
|
2020
1665
|
del self.pids[pid]
|
|
2021
|
-
# Clean micro-batch resources
|
|
2022
1666
|
self._stream_reset(pid)
|
|
2023
1667
|
t = self._stream_timer.pop(pid, None)
|
|
2024
1668
|
if t:
|
|
@@ -2032,7 +1676,7 @@ class Renderer(BaseRenderer):
|
|
|
2032
1676
|
|
|
2033
1677
|
def on_js_ready(self, pid: int) -> None:
|
|
2034
1678
|
"""
|
|
2035
|
-
|
|
1679
|
+
Called by JS via bridge when ready
|
|
2036
1680
|
|
|
2037
1681
|
:param pid: context PID
|
|
2038
1682
|
"""
|
|
@@ -2044,7 +1688,7 @@ class Renderer(BaseRenderer):
|
|
|
2044
1688
|
|
|
2045
1689
|
def _drain_pending_nodes(self, pid: int):
|
|
2046
1690
|
"""
|
|
2047
|
-
|
|
1691
|
+
Flush queued node payloads
|
|
2048
1692
|
|
|
2049
1693
|
:param pid: context PID
|
|
2050
1694
|
"""
|
|
@@ -2064,9 +1708,7 @@ class Renderer(BaseRenderer):
|
|
|
2064
1708
|
elif not replace and hasattr(br, "node"):
|
|
2065
1709
|
br.node.emit(payload)
|
|
2066
1710
|
except Exception:
|
|
2067
|
-
# If something goes wrong, stop draining to avoid dropping further items.
|
|
2068
1711
|
break
|
|
2069
|
-
# stop/clear fallback timer if any
|
|
2070
1712
|
t = self._pending_timer.pop(pid, None)
|
|
2071
1713
|
if t:
|
|
2072
1714
|
try:
|
|
@@ -2076,11 +1718,11 @@ class Renderer(BaseRenderer):
|
|
|
2076
1718
|
|
|
2077
1719
|
def _queue_node(self, pid: int, payload: str, replace: bool):
|
|
2078
1720
|
"""
|
|
2079
|
-
Queue node payload
|
|
1721
|
+
Queue node payload until bridge is ready (with safe fallback)
|
|
2080
1722
|
|
|
2081
1723
|
:param pid: context PID
|
|
2082
|
-
:param payload:
|
|
2083
|
-
:param replace: True if replace
|
|
1724
|
+
:param payload: payload to queue
|
|
1725
|
+
:param replace: True if replace nodes list
|
|
2084
1726
|
"""
|
|
2085
1727
|
q = self._pending_nodes.setdefault(pid, [])
|
|
2086
1728
|
q.append((replace, payload))
|
|
@@ -2090,7 +1732,6 @@ class Renderer(BaseRenderer):
|
|
|
2090
1732
|
t.setInterval(1200) # ms
|
|
2091
1733
|
|
|
2092
1734
|
def on_timeout(pid=pid):
|
|
2093
|
-
# Still not ready? Fallback: flush queued via runJavaScript once.
|
|
2094
1735
|
node = self.get_output_node_by_pid(pid)
|
|
2095
1736
|
if node:
|
|
2096
1737
|
while self._pending_nodes.get(pid):
|
|
@@ -2118,11 +1759,10 @@ class Renderer(BaseRenderer):
|
|
|
2118
1759
|
|
|
2119
1760
|
def _stream_get(self, pid: int) -> tuple[_AppendBuffer, QTimer]:
|
|
2120
1761
|
"""
|
|
2121
|
-
Get
|
|
2122
|
-
Timer is single-shot and (re)started only when there is pending data.
|
|
1762
|
+
Get/create per-PID append buffer and timer
|
|
2123
1763
|
|
|
2124
1764
|
:param pid: context PID
|
|
2125
|
-
:return: (buffer, timer)
|
|
1765
|
+
:return: (buffer, timer)
|
|
2126
1766
|
"""
|
|
2127
1767
|
buf = self._stream_acc.get(pid)
|
|
2128
1768
|
if buf is None:
|
|
@@ -2136,7 +1776,6 @@ class Renderer(BaseRenderer):
|
|
|
2136
1776
|
t.setInterval(self._stream_interval_ms)
|
|
2137
1777
|
|
|
2138
1778
|
def on_timeout(pid=pid):
|
|
2139
|
-
# Flush pending batch and, if more data arrives later, timer will be restarted by _stream_push()
|
|
2140
1779
|
self._stream_flush(pid, force=False)
|
|
2141
1780
|
|
|
2142
1781
|
t.timeout.connect(on_timeout)
|
|
@@ -2146,9 +1785,7 @@ class Renderer(BaseRenderer):
|
|
|
2146
1785
|
|
|
2147
1786
|
def _stream_reset(self, pid: Optional[int]):
|
|
2148
1787
|
"""
|
|
2149
|
-
Reset micro-batch
|
|
2150
|
-
|
|
2151
|
-
Clear buffer, stop timer, clear header and last-flush timestamp.
|
|
1788
|
+
Reset micro-batch state for PID
|
|
2152
1789
|
|
|
2153
1790
|
:param pid: context PID
|
|
2154
1791
|
"""
|
|
@@ -2168,56 +1805,42 @@ class Renderer(BaseRenderer):
|
|
|
2168
1805
|
|
|
2169
1806
|
def _stream_push(self, pid: int, header: str, chunk: str):
|
|
2170
1807
|
"""
|
|
2171
|
-
|
|
2172
|
-
If accumulated size crosses thresholds, flush immediately.
|
|
1808
|
+
Push chunk into buffer and schedule flush
|
|
2173
1809
|
|
|
2174
1810
|
:param pid: context PID
|
|
2175
|
-
:param header: header
|
|
2176
|
-
:param chunk: chunk
|
|
1811
|
+
:param header: optional header (first chunk only)
|
|
1812
|
+
:param chunk: chunk to append
|
|
2177
1813
|
"""
|
|
2178
1814
|
if not chunk:
|
|
2179
1815
|
return
|
|
2180
1816
|
|
|
2181
1817
|
buf, timer = self._stream_get(pid)
|
|
2182
|
-
# Remember last known header for this PID
|
|
2183
1818
|
if header and not self._stream_header.get(pid):
|
|
2184
1819
|
self._stream_header[pid] = header
|
|
2185
1820
|
|
|
2186
|
-
# Append chunk cheaply
|
|
2187
1821
|
buf.append(chunk)
|
|
2188
|
-
|
|
2189
|
-
# Emergency backstop: if buffer is getting too large, flush now
|
|
2190
1822
|
pending_size = getattr(buf, "_size", 0)
|
|
2191
1823
|
if pending_size >= self._stream_emergency_bytes:
|
|
2192
1824
|
self._stream_flush(pid, force=True)
|
|
2193
1825
|
return
|
|
2194
|
-
|
|
2195
|
-
# Size-based early flush for responsiveness
|
|
2196
1826
|
if pending_size >= self._stream_max_bytes:
|
|
2197
1827
|
self._stream_flush(pid, force=True)
|
|
2198
1828
|
return
|
|
2199
|
-
|
|
2200
|
-
# Start timer if not active to flush at ~frame rate
|
|
2201
1829
|
if not timer.isActive():
|
|
2202
1830
|
try:
|
|
2203
1831
|
timer.start()
|
|
2204
1832
|
except Exception:
|
|
2205
|
-
# As a fallback, if timer cannot start, flush synchronously
|
|
2206
1833
|
self._stream_flush(pid, force=True)
|
|
2207
1834
|
|
|
2208
1835
|
def _stream_flush(self, pid: int, force: bool = False):
|
|
2209
1836
|
"""
|
|
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.
|
|
1837
|
+
Flush buffered chunks via QWebChannel (bridge.chunk.emit(name, chunk, type))
|
|
2214
1838
|
|
|
2215
1839
|
:param pid: context PID
|
|
2216
|
-
:param force: True
|
|
1840
|
+
:param force: True if force flush ignoring interval
|
|
2217
1841
|
"""
|
|
2218
1842
|
buf = self._stream_acc.get(pid)
|
|
2219
1843
|
if buf is None or buf.is_empty():
|
|
2220
|
-
# Nothing to send; stop timer if any
|
|
2221
1844
|
t = self._stream_timer.get(pid)
|
|
2222
1845
|
if t and t.isActive():
|
|
2223
1846
|
try:
|
|
@@ -2228,11 +1851,9 @@ class Renderer(BaseRenderer):
|
|
|
2228
1851
|
|
|
2229
1852
|
node = self.get_output_node_by_pid(pid)
|
|
2230
1853
|
if node is None:
|
|
2231
|
-
# Drop buffer if node is gone to avoid leaks
|
|
2232
1854
|
buf.clear()
|
|
2233
1855
|
return
|
|
2234
1856
|
|
|
2235
|
-
# Stop timer for this flush; next push will re-arm it
|
|
2236
1857
|
t = self._stream_timer.get(pid)
|
|
2237
1858
|
if t and t.isActive():
|
|
2238
1859
|
try:
|
|
@@ -2240,22 +1861,18 @@ class Renderer(BaseRenderer):
|
|
|
2240
1861
|
except Exception:
|
|
2241
1862
|
pass
|
|
2242
1863
|
|
|
2243
|
-
# Gather and clear the pending data in one allocation
|
|
2244
1864
|
data = buf.get_and_clear()
|
|
2245
1865
|
name = self._stream_header.get(pid, "") or ""
|
|
2246
1866
|
|
|
2247
1867
|
try:
|
|
2248
1868
|
br = getattr(node.page(), "bridge", None)
|
|
2249
1869
|
if br is not None and hasattr(br, "chunk"):
|
|
2250
|
-
|
|
2251
|
-
br.chunk.emit(name, data)
|
|
1870
|
+
br.chunk.emit(name, data, "text_delta")
|
|
2252
1871
|
else:
|
|
2253
|
-
# Fallback path if bridge not yet connected on this page
|
|
2254
1872
|
node.page().runJavaScript(
|
|
2255
1873
|
f"if (typeof window.appendStream !== 'undefined') appendStream({self.to_json(name)},{self.to_json(data)});"
|
|
2256
1874
|
)
|
|
2257
1875
|
except Exception:
|
|
2258
|
-
# If something goes wrong, attempt a JS fallback once
|
|
2259
1876
|
try:
|
|
2260
1877
|
node.page().runJavaScript(
|
|
2261
1878
|
f"if (typeof window.appendStream !== 'undefined') appendStream({self.to_json(name)},{self.to_json(data)});"
|
|
@@ -2263,16 +1880,13 @@ class Renderer(BaseRenderer):
|
|
|
2263
1880
|
except Exception:
|
|
2264
1881
|
pass
|
|
2265
1882
|
|
|
2266
|
-
# Opportunistic memory release on Linux for very large flushes
|
|
2267
1883
|
try:
|
|
2268
1884
|
if len(data) >= (256 * 1024):
|
|
2269
1885
|
self.auto_cleanup_soft()
|
|
2270
1886
|
except Exception:
|
|
2271
1887
|
pass
|
|
2272
1888
|
|
|
2273
|
-
# Explicitly drop reference to large string so GC/malloc_trim can reclaim sooner
|
|
2274
1889
|
del data
|
|
2275
|
-
|
|
2276
1890
|
self._stream_last_flush[pid] = monotonic()
|
|
2277
1891
|
|
|
2278
1892
|
def eval_js(self, script: str):
|
|
@@ -2292,4 +1906,144 @@ class Renderer(BaseRenderer):
|
|
|
2292
1906
|
try:
|
|
2293
1907
|
node.page().runJavaScript(script, callback)
|
|
2294
1908
|
except Exception:
|
|
2295
|
-
pass
|
|
1909
|
+
pass
|
|
1910
|
+
|
|
1911
|
+
# ------------------------- Helpers: build JSON blocks -------------------------
|
|
1912
|
+
|
|
1913
|
+
def _output_identity(self, ctx: CtxItem) -> Tuple[str, Optional[str], bool]:
|
|
1914
|
+
"""
|
|
1915
|
+
Resolve output identity (name, avatar file:// path) based on preset.
|
|
1916
|
+
|
|
1917
|
+
:param ctx: context item
|
|
1918
|
+
:return: (name, avatar, personalize)
|
|
1919
|
+
"""
|
|
1920
|
+
meta = ctx.meta
|
|
1921
|
+
if meta is None:
|
|
1922
|
+
return self.pids[self.get_or_create_pid(meta)].name_bot if meta else "", None, False
|
|
1923
|
+
preset_id = meta.preset
|
|
1924
|
+
if not preset_id:
|
|
1925
|
+
return self.pids[self.get_or_create_pid(meta)].name_bot, None, False
|
|
1926
|
+
preset = self.window.core.presets.get(preset_id)
|
|
1927
|
+
if preset is None or not preset.ai_personalize:
|
|
1928
|
+
return self.pids[self.get_or_create_pid(meta)].name_bot, None, False
|
|
1929
|
+
name = preset.ai_name or self.pids[self.get_or_create_pid(meta)].name_bot
|
|
1930
|
+
avatar = None
|
|
1931
|
+
if preset.ai_avatar:
|
|
1932
|
+
presets_dir = self.window.core.config.get_user_dir("presets")
|
|
1933
|
+
avatars_dir = os.path.join(presets_dir, "avatars")
|
|
1934
|
+
avatar_path = os.path.join(avatars_dir, preset.ai_avatar)
|
|
1935
|
+
if os.path.exists(avatar_path):
|
|
1936
|
+
avatar = f"{self._file_prefix}{avatar_path}"
|
|
1937
|
+
return name, avatar, bool(preset.ai_personalize)
|
|
1938
|
+
|
|
1939
|
+
def _build_render_block(
|
|
1940
|
+
self,
|
|
1941
|
+
meta: CtxMeta,
|
|
1942
|
+
ctx: CtxItem,
|
|
1943
|
+
input_text: Optional[str],
|
|
1944
|
+
output_text: Optional[str],
|
|
1945
|
+
prev_ctx: Optional[CtxItem] = None,
|
|
1946
|
+
next_ctx: Optional[CtxItem] = None
|
|
1947
|
+
) -> Optional[RenderBlock]:
|
|
1948
|
+
"""
|
|
1949
|
+
Build RenderBlock for given ctx and payloads (input/output).
|
|
1950
|
+
|
|
1951
|
+
- if input_text or output_text is None, that part is skipped.
|
|
1952
|
+
- if ctx.hidden is True, returns None.
|
|
1953
|
+
- if both input_text and output_text are None, returns None.
|
|
1954
|
+
- if meta is None or invalid, returns None.
|
|
1955
|
+
|
|
1956
|
+
:param meta: CtxMeta object
|
|
1957
|
+
:param ctx: CtxItem object
|
|
1958
|
+
:param input_text: Input text (raw, un-formatted)
|
|
1959
|
+
:param prev_ctx: Previous CtxItem (for context, optional)
|
|
1960
|
+
:param next_ctx: Next CtxItem (for context, optional)
|
|
1961
|
+
:return: RenderBlock object or None
|
|
1962
|
+
"""
|
|
1963
|
+
pid = self.get_or_create_pid(meta)
|
|
1964
|
+
if pid is None:
|
|
1965
|
+
return
|
|
1966
|
+
|
|
1967
|
+
block = RenderBlock(id=getattr(ctx, "id", None), meta_id=getattr(meta, "id", None))
|
|
1968
|
+
|
|
1969
|
+
# input
|
|
1970
|
+
if input_text:
|
|
1971
|
+
# Keep raw; formatting is a template duty (escape/BR etc.)
|
|
1972
|
+
block.input = {
|
|
1973
|
+
"type": "user",
|
|
1974
|
+
"name": self.pids[pid].name_user,
|
|
1975
|
+
"avatar_img": None, # no user avatar by default
|
|
1976
|
+
"text": str(input_text),
|
|
1977
|
+
"timestamp": ctx.input_timestamp if hasattr(ctx, "input_timestamp") else None,
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
# output
|
|
1981
|
+
if output_text:
|
|
1982
|
+
# Pre/post format raw markdown via Helpers to preserve placeholders ([!cmd], think) and workdir tokens.
|
|
1983
|
+
md_src = self.helpers.pre_format_text(output_text)
|
|
1984
|
+
md_text = self.helpers.post_format_text(md_src)
|
|
1985
|
+
name, avatar, personalize = self._output_identity(ctx)
|
|
1986
|
+
|
|
1987
|
+
# tool output visibility (agent step / commands)
|
|
1988
|
+
is_cmd = (
|
|
1989
|
+
next_ctx is not None and
|
|
1990
|
+
next_ctx.internal and
|
|
1991
|
+
(len(ctx.cmds) > 0 or (ctx.extra_ctx is not None and len(ctx.extra_ctx) > 0))
|
|
1992
|
+
)
|
|
1993
|
+
tool_output = ""
|
|
1994
|
+
tool_output_visible = False
|
|
1995
|
+
if is_cmd:
|
|
1996
|
+
if ctx.results is not None and len(ctx.results) > 0 \
|
|
1997
|
+
and isinstance(ctx.extra, dict) and "agent_step" in ctx.extra:
|
|
1998
|
+
tool_output = self.helpers.format_cmd_text(str(ctx.input), indent=True)
|
|
1999
|
+
tool_output_visible = True
|
|
2000
|
+
else:
|
|
2001
|
+
tool_output = self.helpers.format_cmd_text(str(next_ctx.input), indent=True)
|
|
2002
|
+
tool_output_visible = True
|
|
2003
|
+
elif ctx.results is not None and len(ctx.results) > 0 \
|
|
2004
|
+
and isinstance(ctx.extra, dict) and "agent_step" in ctx.extra:
|
|
2005
|
+
tool_output = self.helpers.format_cmd_text(str(ctx.input), indent=True)
|
|
2006
|
+
|
|
2007
|
+
# plugin-driven extra (HTML) – keep as-is to preserve functionality
|
|
2008
|
+
tool_extra_html = self.body.prepare_tool_extra(ctx)
|
|
2009
|
+
|
|
2010
|
+
block.output = {
|
|
2011
|
+
"type": "bot",
|
|
2012
|
+
"name": name or self.pids[pid].name_bot,
|
|
2013
|
+
"avatar_img": avatar,
|
|
2014
|
+
"text": md_text,
|
|
2015
|
+
"timestamp": ctx.output_timestamp if hasattr(ctx, "output_timestamp") else None,
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
# extras (images/files/urls/actions)
|
|
2019
|
+
images, files, urls, extra_actions = self.body.build_extras_dicts(ctx, pid)
|
|
2020
|
+
block.images = images
|
|
2021
|
+
block.files = files
|
|
2022
|
+
block.urls = urls
|
|
2023
|
+
|
|
2024
|
+
# docs as raw data -> rendered in JS
|
|
2025
|
+
docs_norm = []
|
|
2026
|
+
if self.window.core.config.get('ctx.sources'):
|
|
2027
|
+
if ctx.doc_ids is not None and len(ctx.doc_ids) > 0:
|
|
2028
|
+
docs_norm = self.body.normalize_docs(ctx.doc_ids)
|
|
2029
|
+
|
|
2030
|
+
block.extra.update({
|
|
2031
|
+
"tool_output": tool_output,
|
|
2032
|
+
"tool_output_visible": tool_output_visible,
|
|
2033
|
+
"tool_extra_html": tool_extra_html,
|
|
2034
|
+
"docs": docs_norm,
|
|
2035
|
+
"footer_icons": True,
|
|
2036
|
+
"personalize": personalize,
|
|
2037
|
+
})
|
|
2038
|
+
block.extra.update(extra_actions)
|
|
2039
|
+
|
|
2040
|
+
# carry ctx.extra flags as-is (do not collide with our own keys)
|
|
2041
|
+
if isinstance(ctx.extra, dict):
|
|
2042
|
+
block.extra.setdefault("ctx_extra", ctx.extra)
|
|
2043
|
+
|
|
2044
|
+
# debug
|
|
2045
|
+
if self.is_debug():
|
|
2046
|
+
print(block.debug())
|
|
2047
|
+
block.extra["debug_html"] = self.append_debug(ctx, pid, "output")
|
|
2048
|
+
|
|
2049
|
+
return block
|