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,14 +6,14 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2025.09.
|
|
9
|
+
# Updated Date: 2025.09.13 06:05:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import os
|
|
13
13
|
from json import dumps as _json_dumps
|
|
14
14
|
from random import shuffle as _shuffle
|
|
15
15
|
|
|
16
|
-
from typing import Optional, List, Dict
|
|
16
|
+
from typing import Optional, List, Dict, Tuple
|
|
17
17
|
|
|
18
18
|
from pygpt_net.core.text.utils import elide_filename
|
|
19
19
|
from pygpt_net.core.events import Event
|
|
@@ -208,33 +208,34 @@ class Body:
|
|
|
208
208
|
"""
|
|
209
209
|
|
|
210
210
|
def __init__(self, window=None):
|
|
211
|
+
"""
|
|
212
|
+
Initialize Body with reference to main window and syntax highlighter.
|
|
213
|
+
|
|
214
|
+
:param window: Window reference
|
|
215
|
+
"""
|
|
211
216
|
self.window = window
|
|
212
217
|
self.highlight = SyntaxHighlight(window)
|
|
213
218
|
self._tip_keys = tuple(f"output.tips.{i}" for i in range(1, self.NUM_TIPS + 1))
|
|
214
219
|
self._syntax_dark = (
|
|
215
|
-
"dracula",
|
|
216
|
-
"
|
|
217
|
-
"
|
|
218
|
-
"gruvbox-dark",
|
|
219
|
-
"inkpot",
|
|
220
|
-
"material",
|
|
221
|
-
"monokai",
|
|
222
|
-
"native",
|
|
223
|
-
"nord",
|
|
224
|
-
"nord-darker",
|
|
225
|
-
"one-dark",
|
|
226
|
-
"paraiso-dark",
|
|
227
|
-
"rrt",
|
|
228
|
-
"solarized-dark",
|
|
229
|
-
"stata-dark",
|
|
230
|
-
"vim",
|
|
231
|
-
"zenburn",
|
|
220
|
+
"dracula", "fruity", "github-dark", "gruvbox-dark", "inkpot", "material",
|
|
221
|
+
"monokai", "native", "nord", "nord-darker", "one-dark", "paraiso-dark",
|
|
222
|
+
"rrt", "solarized-dark", "stata-dark", "vim", "zenburn",
|
|
232
223
|
)
|
|
233
224
|
|
|
234
225
|
def is_timestamp_enabled(self) -> bool:
|
|
226
|
+
"""
|
|
227
|
+
Check if timestamp display is enabled in config.
|
|
228
|
+
|
|
229
|
+
:return: True if enabled, False otherwise.
|
|
230
|
+
"""
|
|
235
231
|
return self.window.core.config.get('output_timestamp')
|
|
236
232
|
|
|
237
233
|
def prepare_styles(self) -> str:
|
|
234
|
+
"""
|
|
235
|
+
Prepare combined CSS styles for the web view.
|
|
236
|
+
|
|
237
|
+
:return: Combined CSS string.
|
|
238
|
+
"""
|
|
238
239
|
cfg = self.window.core.config
|
|
239
240
|
fonts_path = os.path.join(cfg.get_app_path(), "data", "fonts").replace("\\", "/")
|
|
240
241
|
syntax_style = self.window.core.config.get("render.code_syntax") or "default"
|
|
@@ -250,12 +251,25 @@ class Body:
|
|
|
250
251
|
return "\n".join(parts)
|
|
251
252
|
|
|
252
253
|
def prepare_action_icons(self, ctx: CtxItem) -> str:
|
|
254
|
+
"""
|
|
255
|
+
Prepare HTML for message-level action icons.
|
|
256
|
+
|
|
257
|
+
:param ctx: CtxItem
|
|
258
|
+
:return: HTML string or empty if no icons.
|
|
259
|
+
"""
|
|
253
260
|
icons_html = "".join(self.get_action_icons(ctx, all=True))
|
|
254
261
|
if icons_html:
|
|
255
262
|
return f'<div class="action-icons" data-id="{ctx.id}">{icons_html}</div>'
|
|
256
263
|
return ""
|
|
257
264
|
|
|
258
265
|
def get_action_icons(self, ctx: CtxItem, all: bool = False) -> List[str]:
|
|
266
|
+
"""
|
|
267
|
+
Return HTML snippets for message-level action icons.
|
|
268
|
+
|
|
269
|
+
:param ctx: CtxItem
|
|
270
|
+
:param all: If True, return all icons; otherwise, return only available ones.
|
|
271
|
+
:return: List of HTML strings for icons.
|
|
272
|
+
"""
|
|
259
273
|
icons: List[str] = []
|
|
260
274
|
if ctx.output:
|
|
261
275
|
cid = ctx.id
|
|
@@ -275,12 +289,71 @@ class Body:
|
|
|
275
289
|
f'<a href="extra-join:{cid}" class="action-icon edit-icon" data-id="{cid}" role="button"><span class="cmd">{self.get_icon("playlist_add", t("ctx.extra.join"), ctx)}</span></a>')
|
|
276
290
|
return icons
|
|
277
291
|
|
|
278
|
-
def
|
|
292
|
+
def get_action_icon_data(self, ctx: CtxItem) -> List[Dict]:
|
|
293
|
+
"""
|
|
294
|
+
Return raw data for message-level action icons (href/title/icon path).
|
|
295
|
+
This allows JS templates to render actions without Python-side HTML.
|
|
296
|
+
|
|
297
|
+
1. extra-audio-read
|
|
298
|
+
2. extra-copy
|
|
299
|
+
3. extra-replay
|
|
300
|
+
4. extra-edit
|
|
301
|
+
5. extra-delete
|
|
302
|
+
6. extra-join (if not the first item)
|
|
303
|
+
7. (future actions...)
|
|
304
|
+
|
|
305
|
+
:param ctx: CtxItem
|
|
306
|
+
:return: List of action dicts
|
|
307
|
+
"""
|
|
308
|
+
items: List[Dict] = []
|
|
309
|
+
if ctx.output:
|
|
310
|
+
cid = ctx.id
|
|
311
|
+
t = trans
|
|
312
|
+
app_path = self.window.core.config.get_app_path()
|
|
313
|
+
def icon_path(name: str) -> str:
|
|
314
|
+
return os.path.join(app_path, "data", "icons", f"{name}.svg").replace("\\", "/")
|
|
315
|
+
|
|
316
|
+
items.append({"href": f"extra-audio-read:{cid}", "title": t("ctx.extra.audio"), "icon": f"file://{icon_path('volume')}", "id": cid})
|
|
317
|
+
items.append({"href": f"extra-copy:{cid}", "title": t("ctx.extra.copy"), "icon": f"file://{icon_path('copy')}", "id": cid})
|
|
318
|
+
items.append({"href": f"extra-replay:{cid}", "title": t("ctx.extra.reply"), "icon": f"file://{icon_path('reload')}", "id": cid})
|
|
319
|
+
items.append({"href": f"extra-edit:{cid}", "title": t("ctx.extra.edit"), "icon": f"file://{icon_path('edit')}", "id": cid})
|
|
320
|
+
items.append({"href": f"extra-delete:{cid}", "title": t("ctx.extra.delete"), "icon": f"file://{icon_path('delete')}", "id": cid})
|
|
321
|
+
if not self.window.core.ctx.is_first_item(cid):
|
|
322
|
+
items.append({"href": f"extra-join:{cid}", "title": t("ctx.extra.join"), "icon": f"file://{icon_path('playlist_add')}", "id": cid})
|
|
323
|
+
return items
|
|
324
|
+
|
|
325
|
+
def get_icon(
|
|
326
|
+
self,
|
|
327
|
+
icon: str,
|
|
328
|
+
title: Optional[str] = None,
|
|
329
|
+
item: Optional[CtxItem] = None
|
|
330
|
+
) -> str:
|
|
331
|
+
"""
|
|
332
|
+
Get HTML for an icon image with title and optional data-id.
|
|
333
|
+
|
|
334
|
+
:param icon: Icon name (without extension)
|
|
335
|
+
:param title: Icon title (tooltip)
|
|
336
|
+
:param item: Optional CtxItem to get data-id from
|
|
337
|
+
:return: HTML string
|
|
338
|
+
"""
|
|
279
339
|
app_path = self.window.core.config.get_app_path()
|
|
280
340
|
icon_path = os.path.join(app_path, "data", "icons", f"{icon}.svg")
|
|
281
341
|
return f'<img src="file://{icon_path}" class="action-img" title="{title}" alt="{title}" data-id="{item.id}">'
|
|
282
342
|
|
|
283
|
-
def get_image_html(
|
|
343
|
+
def get_image_html(
|
|
344
|
+
self,
|
|
345
|
+
url: str,
|
|
346
|
+
num: Optional[int] = None,
|
|
347
|
+
num_all: Optional[int] = None
|
|
348
|
+
) -> str:
|
|
349
|
+
"""
|
|
350
|
+
Get HTML for an image or video link with optional numbering.
|
|
351
|
+
|
|
352
|
+
:param url: Image or video URL
|
|
353
|
+
:param num: Optional index (1-based)
|
|
354
|
+
:param num_all: Optional total number of images/videos
|
|
355
|
+
:return: HTML string
|
|
356
|
+
"""
|
|
284
357
|
url, path = self.window.core.filesystem.extract_local_url(url)
|
|
285
358
|
basename = os.path.basename(path)
|
|
286
359
|
ext = os.path.splitext(basename)[1].lower()
|
|
@@ -301,7 +374,20 @@ class Body:
|
|
|
301
374
|
'''
|
|
302
375
|
return f'<div class="extra-src-img-box" title="{url}"><div class="img-outer"><div class="img-wrapper"><a href="{url}"><img src="{path}" class="image"></a></div><a href="{url}" class="title">{elide_filename(basename)}</a></div></div><br/>'
|
|
303
376
|
|
|
304
|
-
def get_url_html(
|
|
377
|
+
def get_url_html(
|
|
378
|
+
self,
|
|
379
|
+
url: str,
|
|
380
|
+
num: Optional[int] = None,
|
|
381
|
+
num_all: Optional[int] = None
|
|
382
|
+
) -> str:
|
|
383
|
+
"""
|
|
384
|
+
Get HTML for a URL link with icon and optional numbering.
|
|
385
|
+
|
|
386
|
+
:param url: URL string
|
|
387
|
+
:param num: Optional index (1-based)
|
|
388
|
+
:param num_all: Optional total number of URLs
|
|
389
|
+
:return: HTML string
|
|
390
|
+
"""
|
|
305
391
|
app_path = self.window.core.config.get_app_path()
|
|
306
392
|
icon_path = os.path.join(app_path, "data", "icons", "url.svg").replace("\\", "/")
|
|
307
393
|
icon = f'<img src="file://{icon_path}" class="extra-src-icon">'
|
|
@@ -309,6 +395,12 @@ class Body:
|
|
|
309
395
|
return f'{icon}<a href="{url}" title="{url}">{url}</a> <small>{num_str}</small>'
|
|
310
396
|
|
|
311
397
|
def get_docs_html(self, docs: List[Dict]) -> str:
|
|
398
|
+
"""
|
|
399
|
+
Get HTML for document references.
|
|
400
|
+
|
|
401
|
+
:param docs: List of document dicts {uuid: {meta_dict}}
|
|
402
|
+
:return: HTML string or empty if no docs.
|
|
403
|
+
"""
|
|
312
404
|
html_parts: List[str] = []
|
|
313
405
|
src_parts: List[str] = []
|
|
314
406
|
num = 1
|
|
@@ -338,7 +430,20 @@ class Body:
|
|
|
338
430
|
|
|
339
431
|
return "".join(html_parts)
|
|
340
432
|
|
|
341
|
-
def get_file_html(
|
|
433
|
+
def get_file_html(
|
|
434
|
+
self,
|
|
435
|
+
url: str,
|
|
436
|
+
num: Optional[int] = None,
|
|
437
|
+
num_all: Optional[int] = None
|
|
438
|
+
) -> str:
|
|
439
|
+
"""
|
|
440
|
+
Get HTML for a file link with icon and optional numbering.
|
|
441
|
+
|
|
442
|
+
:param url: File URL
|
|
443
|
+
:param num: Optional file index (1-based)
|
|
444
|
+
:param num_all: Optional total number of files
|
|
445
|
+
:return: HTML string
|
|
446
|
+
"""
|
|
342
447
|
app_path = self.window.core.config.get_app_path()
|
|
343
448
|
icon_path = os.path.join(app_path, "data", "icons", "attachments.svg").replace("\\", "/")
|
|
344
449
|
icon = f'<img src="file://{icon_path}" class="extra-src-icon">'
|
|
@@ -347,6 +452,12 @@ class Body:
|
|
|
347
452
|
return f'{icon} <b>{num_str}</b> <a href="{url}">{path}</a>'
|
|
348
453
|
|
|
349
454
|
def prepare_tool_extra(self, ctx: CtxItem) -> str:
|
|
455
|
+
"""
|
|
456
|
+
Prepare extra HTML for tool/plugin output.
|
|
457
|
+
|
|
458
|
+
:param ctx: CtxItem
|
|
459
|
+
:return: HTML string or empty if no extra.
|
|
460
|
+
"""
|
|
350
461
|
extra = ctx.extra
|
|
351
462
|
if not extra:
|
|
352
463
|
return ""
|
|
@@ -384,6 +495,11 @@ class Body:
|
|
|
384
495
|
return "".join(parts)
|
|
385
496
|
|
|
386
497
|
def get_all_tips(self) -> str:
|
|
498
|
+
"""
|
|
499
|
+
Get all tips as a JSON array string.
|
|
500
|
+
|
|
501
|
+
:return: JSON array string of tips or "[]" if disabled.
|
|
502
|
+
"""
|
|
387
503
|
if not self.window.core.config.get("layout.tooltips", False):
|
|
388
504
|
return "[]"
|
|
389
505
|
|
|
@@ -396,7 +512,149 @@ class Body:
|
|
|
396
512
|
_shuffle(tips)
|
|
397
513
|
return _json_dumps(tips)
|
|
398
514
|
|
|
515
|
+
def _extract_local_url(self, url: str) -> Tuple[str, str]:
|
|
516
|
+
"""
|
|
517
|
+
Extract local URL and path using filesystem helper.
|
|
518
|
+
|
|
519
|
+
On failure, return (url, url).
|
|
520
|
+
|
|
521
|
+
:param url: URL to extract.
|
|
522
|
+
:return: Tuple of (url, path).
|
|
523
|
+
"""
|
|
524
|
+
try:
|
|
525
|
+
return self.window.core.filesystem.extract_local_url(url)
|
|
526
|
+
except Exception:
|
|
527
|
+
return url, url
|
|
528
|
+
|
|
529
|
+
def build_extras_dicts(self, ctx: CtxItem, pid: int) -> Tuple[dict, dict, dict, dict]:
|
|
530
|
+
"""
|
|
531
|
+
Build images/files/urls raw dicts to be rendered by JS templates.
|
|
532
|
+
|
|
533
|
+
1-based indexing for keys as strings: "1", "2", ...
|
|
534
|
+
0-based indexing is inconvenient in JS templates.
|
|
535
|
+
1-based indexing allows to show [n/m] in titles.
|
|
536
|
+
|
|
537
|
+
1. images_dict = { "1": {url, path, basename, ext, is_video, webm_path}, ... }
|
|
538
|
+
2. files_dict = { "1": {url, path}, ... }
|
|
539
|
+
3. urls_dict = { "1": {url}, ... }
|
|
540
|
+
4. actions_dict = { "actions": [ {href, title, icon, id}, ... ] } # message-level actions
|
|
541
|
+
|
|
542
|
+
:param ctx: CtxItem
|
|
543
|
+
:param pid: Process ID
|
|
544
|
+
:return: Tuple of (images_dict, files_dict, urls_dict, actions_dict)
|
|
545
|
+
"""
|
|
546
|
+
images = {}
|
|
547
|
+
files = {}
|
|
548
|
+
urls = {}
|
|
549
|
+
|
|
550
|
+
# images
|
|
551
|
+
if ctx.images:
|
|
552
|
+
video_exts = (".mp4", ".webm", ".ogg", ".mov", ".avi", ".mkv")
|
|
553
|
+
n = 1
|
|
554
|
+
for img in ctx.images:
|
|
555
|
+
if img is None:
|
|
556
|
+
continue
|
|
557
|
+
try:
|
|
558
|
+
url, path = self._extract_local_url(img)
|
|
559
|
+
basename = os.path.basename(path)
|
|
560
|
+
ext = os.path.splitext(basename)[1].lower()
|
|
561
|
+
is_video = ext in video_exts
|
|
562
|
+
webm_path = ""
|
|
563
|
+
if is_video and ext != ".webm":
|
|
564
|
+
wp = os.path.splitext(path)[0] + ".webm"
|
|
565
|
+
if os.path.exists(wp):
|
|
566
|
+
webm_path = wp
|
|
567
|
+
images[str(n)] = {
|
|
568
|
+
"url": url,
|
|
569
|
+
"path": path,
|
|
570
|
+
"basename": basename,
|
|
571
|
+
"ext": ext,
|
|
572
|
+
"is_video": is_video,
|
|
573
|
+
"webm_path": webm_path,
|
|
574
|
+
}
|
|
575
|
+
n += 1
|
|
576
|
+
except Exception:
|
|
577
|
+
pass
|
|
578
|
+
|
|
579
|
+
# files
|
|
580
|
+
if ctx.files:
|
|
581
|
+
n = 1
|
|
582
|
+
for f in ctx.files:
|
|
583
|
+
try:
|
|
584
|
+
url, path = self._extract_local_url(f)
|
|
585
|
+
files[str(n)] = {
|
|
586
|
+
"url": url,
|
|
587
|
+
"path": path,
|
|
588
|
+
}
|
|
589
|
+
n += 1
|
|
590
|
+
except Exception:
|
|
591
|
+
pass
|
|
592
|
+
|
|
593
|
+
# urls
|
|
594
|
+
if ctx.urls:
|
|
595
|
+
n = 1
|
|
596
|
+
for u in ctx.urls:
|
|
597
|
+
try:
|
|
598
|
+
urls[str(n)] = {"url": u}
|
|
599
|
+
n += 1
|
|
600
|
+
except Exception:
|
|
601
|
+
pass
|
|
602
|
+
|
|
603
|
+
# actions (message-level) – raw data for icons (href/title/icon)
|
|
604
|
+
actions = self.get_action_icon_data(ctx)
|
|
605
|
+
|
|
606
|
+
return images, files, urls, {"actions": actions}
|
|
607
|
+
|
|
608
|
+
def normalize_docs(self, doc_ids) -> list[dict]:
|
|
609
|
+
"""
|
|
610
|
+
Normalize ctx.doc_ids into a list of {"uuid": str, "meta": dict}.
|
|
611
|
+
Accepts original shape: List[Dict[uuid -> meta_dict]] or already normalized.
|
|
612
|
+
|
|
613
|
+
Returns empty list on failure.
|
|
614
|
+
|
|
615
|
+
Example input:
|
|
616
|
+
[
|
|
617
|
+
{"123e4567-e89b-12d3-a456-426614174000": {"title": "Document 1", "source": "file1.txt"}},
|
|
618
|
+
{"123e4567-e89b-12d3-a456-426614174001": {"title": "Document 2", "source": "file2.txt"}}
|
|
619
|
+
]
|
|
620
|
+
Example output:
|
|
621
|
+
[
|
|
622
|
+
{"uuid": "123e4567-e89b-12d3-a456-426614174000", "meta": {"title": "Document 1", "source": "file1.txt"}},
|
|
623
|
+
{"uuid": "123e4567-e89b-12d3-a456-426614174001", "meta": {"title": "Document 2", "source": "file2.txt"}}
|
|
624
|
+
]
|
|
625
|
+
|
|
626
|
+
:param doc_ids: List of document IDs in original or normalized shape.
|
|
627
|
+
:return: List of normalized document dicts.
|
|
628
|
+
"""
|
|
629
|
+
normalized = []
|
|
630
|
+
try:
|
|
631
|
+
for item in doc_ids or []:
|
|
632
|
+
if isinstance(item, dict):
|
|
633
|
+
# Already normalized?
|
|
634
|
+
if 'uuid' in item and 'meta' in item and isinstance(item['meta'], dict):
|
|
635
|
+
normalized.append({
|
|
636
|
+
"uuid": str(item['uuid']),
|
|
637
|
+
"meta": dict(item['meta']),
|
|
638
|
+
})
|
|
639
|
+
continue
|
|
640
|
+
# Original shape: { uuid: { ... } }
|
|
641
|
+
keys = list(item.keys())
|
|
642
|
+
if len(keys) == 1:
|
|
643
|
+
uuid = str(keys[0])
|
|
644
|
+
meta = item[uuid]
|
|
645
|
+
if isinstance(meta, dict):
|
|
646
|
+
normalized.append({"uuid": uuid, "meta": dict(meta)})
|
|
647
|
+
except Exception:
|
|
648
|
+
pass
|
|
649
|
+
return normalized
|
|
650
|
+
|
|
399
651
|
def get_html(self, pid: int) -> str:
|
|
652
|
+
"""
|
|
653
|
+
Build full HTML for the web view body.
|
|
654
|
+
|
|
655
|
+
:param pid: Process ID to embed in JS.
|
|
656
|
+
:return: Full HTML string.
|
|
657
|
+
"""
|
|
400
658
|
cfg_get = self.window.core.config.get
|
|
401
659
|
style = cfg_get("theme.style", "blocks")
|
|
402
660
|
classes = ["theme-" + style]
|
|
@@ -419,6 +677,10 @@ class Body:
|
|
|
419
677
|
run_path = os.path.join(app_path, "data", "icons", "play.svg").replace("\\", "/")
|
|
420
678
|
menu_path = os.path.join(app_path, "data", "icons", "menu.svg").replace("\\", "/")
|
|
421
679
|
|
|
680
|
+
url_path = os.path.join(app_path, "data", "icons", "url.svg").replace("\\", "/")
|
|
681
|
+
attach_path = os.path.join(app_path, "data", "icons", "attachments.svg").replace("\\", "/")
|
|
682
|
+
db_path = os.path.join(app_path, "data", "icons", "db.svg").replace("\\", "/")
|
|
683
|
+
|
|
422
684
|
icons_js = (
|
|
423
685
|
f'window.ICON_EXPAND="file://{expand_path}";'
|
|
424
686
|
f'window.ICON_COLLAPSE="file://{collapse_path}";'
|
|
@@ -426,6 +688,9 @@ class Body:
|
|
|
426
688
|
f'window.ICON_CODE_PREVIEW="file://{preview_path}";'
|
|
427
689
|
f'window.ICON_CODE_RUN="file://{run_path}";'
|
|
428
690
|
f'window.ICON_CODE_MENU="file://{menu_path}";'
|
|
691
|
+
f'window.ICON_URL="file://{url_path}";'
|
|
692
|
+
f'window.ICON_ATTACHMENTS="file://{attach_path}";'
|
|
693
|
+
f'window.ICON_DB="file://{db_path}";'
|
|
429
694
|
)
|
|
430
695
|
|
|
431
696
|
t_copy = trans('ctx.extra.copy_code')
|
|
@@ -434,6 +699,7 @@ class Body:
|
|
|
434
699
|
t_copied = trans('ctx.extra.copied')
|
|
435
700
|
t_preview = trans('ctx.extra.preview')
|
|
436
701
|
t_run = trans('ctx.extra.run')
|
|
702
|
+
t_doc_prefix = trans("chat.prefix.doc")
|
|
437
703
|
|
|
438
704
|
locales_js = (
|
|
439
705
|
f'window.LOCALE_COPY={_json_dumps(t_copy)};'
|
|
@@ -442,6 +708,7 @@ class Body:
|
|
|
442
708
|
f'window.LOCALE_RUN={_json_dumps(t_run)};'
|
|
443
709
|
f'window.LOCALE_COLLAPSE={_json_dumps(t_collapse)};'
|
|
444
710
|
f'window.LOCALE_EXPAND={_json_dumps(t_expand)};'
|
|
711
|
+
f'window.LOCALE_DOC_PREFIX={_json_dumps(t_doc_prefix)};'
|
|
445
712
|
)
|
|
446
713
|
|
|
447
714
|
syntax_style = cfg_get("render.code_syntax") or "default"
|
|
@@ -24,6 +24,7 @@ class Helpers:
|
|
|
24
24
|
#_RE_TOOL_TAG = re.compile(r"<tool>(.*?)</tool>", re.DOTALL)
|
|
25
25
|
_RE_TOOL_TAG = re.compile(r"<tool>(.*?)</tool>", re.DOTALL)
|
|
26
26
|
_RE_THINK_TAG = re.compile(r"<think>(.*?)</think>", re.DOTALL)
|
|
27
|
+
_RE_EXECUTE_TAG = re.compile(r"<execute>(.*?)</execute>", re.DOTALL)
|
|
27
28
|
#_RE_MATH_PARENS = re.compile(r"\\\((.*?)\\\)", re.DOTALL)
|
|
28
29
|
|
|
29
30
|
def __init__(self, window=None):
|
|
@@ -80,6 +81,15 @@ class Helpers:
|
|
|
80
81
|
g = m.group(1).replace("\n", "<br>")
|
|
81
82
|
return f'[!think]{html.escape(g)}[/!think]'
|
|
82
83
|
|
|
84
|
+
def _repl_execute(self, m: re.Match) -> str:
|
|
85
|
+
"""
|
|
86
|
+
Replace execute tags with HTML paragraph
|
|
87
|
+
|
|
88
|
+
:param m: regex match object
|
|
89
|
+
:return: formatted HTML string
|
|
90
|
+
"""
|
|
91
|
+
return f'[!exec]{html.escape(m.group(1))}[/!exec]'
|
|
92
|
+
|
|
83
93
|
def _repl_math_fix(self, m: re.Match) -> str:
|
|
84
94
|
"""
|
|
85
95
|
Fix math formula by replacing < and > with < and > inside \\( ... \\)
|
|
@@ -127,6 +137,21 @@ class Helpers:
|
|
|
127
137
|
|
|
128
138
|
return s
|
|
129
139
|
|
|
140
|
+
def replace_execute_tags(self, text: str) -> str:
|
|
141
|
+
"""
|
|
142
|
+
Replace execute tags
|
|
143
|
+
|
|
144
|
+
:param text:
|
|
145
|
+
:return: replaced text
|
|
146
|
+
"""
|
|
147
|
+
s = text
|
|
148
|
+
|
|
149
|
+
# --- execute tags ---
|
|
150
|
+
if "<execute>" in s and "</execute>" in s:
|
|
151
|
+
s = self._RE_EXECUTE_TAG.sub(self._repl_execute, s)
|
|
152
|
+
|
|
153
|
+
return s
|
|
154
|
+
|
|
130
155
|
def pre_format_text(self, text: str) -> str:
|
|
131
156
|
"""
|
|
132
157
|
Pre-format text
|
|
@@ -151,6 +176,7 @@ class Helpers:
|
|
|
151
176
|
# replace tags with markdown placeholders (will be converted to HTML in JS runtime)
|
|
152
177
|
s = self.replace_code_tags(text.strip())
|
|
153
178
|
s = self.replace_think_tags(s)
|
|
179
|
+
s = self.replace_execute_tags(s)
|
|
154
180
|
|
|
155
181
|
# replace workdir token
|
|
156
182
|
if "%workdir%" in s:
|