pygpt-net 2.6.48__py3-none-any.whl → 2.6.50__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 CHANGED
@@ -1,3 +1,13 @@
1
+ 2.6.50 (2025-09-16)
2
+
3
+ - Optimized: Improved memory cleanup when switching profiles and unloading tabs.
4
+ - Fix: Resolved missing PID data in text output.
5
+ - Fix: Enhanced real-time parsing of execute tags.
6
+
7
+ 2.6.49 (2025-09-16)
8
+
9
+ - Fixed: Occasional crashes when focusing on an output container unloaded from memory in the second column.
10
+
1
11
  2.6.48 (2025-09-15)
2
12
 
3
13
  - Added: auto-loading of next items to the list of contexts when scrolling to the end of the list.
pygpt_net/__init__.py CHANGED
@@ -6,15 +6,15 @@
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.15 00:00:00 #
9
+ # Updated Date: 2025.09.16 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  __author__ = "Marcin Szczygliński"
13
13
  __copyright__ = "Copyright 2025, Marcin Szczygliński"
14
14
  __credits__ = ["Marcin Szczygliński"]
15
15
  __license__ = "MIT"
16
- __version__ = "2.6.48"
17
- __build__ = "2025-09-15"
16
+ __version__ = "2.6.50"
17
+ __build__ = "2025-09-16"
18
18
  __maintainer__ = "Marcin Szczygliński"
19
19
  __github__ = "https://github.com/szczyglis-dev/py-gpt"
20
20
  __report__ = "https://github.com/szczyglis-dev/py-gpt/issues"
@@ -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.15 22:00:00 #
9
+ # Updated Date: 2025.09.16 02:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Optional, List
@@ -479,7 +479,8 @@ class Ctx:
479
479
  id: int,
480
480
  restore_model: bool = True,
481
481
  select_idx: Optional[int] = None,
482
- new_tab: Optional[bool] = False
482
+ new_tab: Optional[bool] = False,
483
+ no_fresh: bool = False
483
484
  ):
484
485
  """
485
486
  Load ctx data
@@ -488,6 +489,7 @@ class Ctx:
488
489
  :param restore_model: restore model if defined in ctx
489
490
  :param select_idx: select index on list after loading
490
491
  :param new_tab: open in new tab
492
+ :param no_fresh: do not fresh output
491
493
  """
492
494
  if new_tab:
493
495
  col_idx = self.window.controller.ui.tabs.column_idx
@@ -500,7 +502,7 @@ class Ctx:
500
502
  if meta is not None:
501
503
  self.set_group(meta.group_id)
502
504
 
503
- if meta is not None:
505
+ if meta is not None and not no_fresh:
504
506
  self.fresh_output(meta)
505
507
 
506
508
  self.reload_config()
@@ -445,7 +445,7 @@ class Plugins:
445
445
  """Reload plugins"""
446
446
  self.window.core.plugins.reload_all()
447
447
  self.setup()
448
- self.settings.setup()
448
+ self.settings.init()
449
449
  self.update()
450
450
 
451
451
  def save_all(self):
@@ -6,14 +6,12 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.24 23:00:00 #
9
+ # Updated Date: 2025.09.16 02:00:00 #
10
10
  # ================================================== #
11
- import gc
12
- from typing import Any, Optional, Tuple
13
11
 
14
- from PySide6.QtCore import QTimer, QUrl
15
- from PySide6.QtWebEngineCore import QWebEnginePage
12
+ from typing import Any, Optional, Tuple
16
13
 
14
+ from PySide6.QtCore import QTimer
17
15
  from pygpt_net.core.events import AppEvent, RenderEvent
18
16
  from pygpt_net.core.tabs.tab import Tab
19
17
  from pygpt_net.item.ctx import CtxMeta
@@ -363,7 +361,7 @@ class Tabs:
363
361
  if tab.type == Tab.TAB_CHAT and self.column_idx == 1 and not getattr(tab, "loaded", False):
364
362
  meta = self.window.core.ctx.get_meta_by_id(tab.data_id)
365
363
  if meta is not None:
366
- self.window.controller.ctx.load(meta.id)
364
+ self.window.controller.ctx.load(meta.id, no_fresh=True)
367
365
  tab.loaded = True
368
366
 
369
367
  current_ctx = self.window.core.ctx.get_current()
@@ -23,6 +23,7 @@ class PidData:
23
23
  files_appended: list = field(default_factory=list)
24
24
  _buffer: io.StringIO = field(default_factory=io.StringIO)
25
25
  is_cmd: bool = False
26
+ loaded: bool = False
26
27
 
27
28
  def __init__(self, pid, meta=None):
28
29
  """Pid Data"""
@@ -33,6 +34,7 @@ class PidData:
33
34
  self.files_appended = []
34
35
  self._buffer = io.StringIO()
35
36
  self.is_cmd = False
37
+ self.loaded = False
36
38
 
37
39
  @property
38
40
  def buffer(self) -> str:
@@ -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.14 20:00:00 #
9
+ # Updated Date: 2025.09.16 02:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import gc
@@ -1274,12 +1274,7 @@ class Renderer(BaseRenderer):
1274
1274
  pass
1275
1275
  self._bridge_ready[pid] = False
1276
1276
  self._pending_nodes[pid] = []
1277
- node.hide()
1278
- p = node.page()
1279
- p.triggerAction(QWebEnginePage.Stop)
1280
- p.setUrl(QUrl("about:blank"))
1281
- p.history().clear()
1282
- p.setLifecycleState(QWebEnginePage.LifecycleState.Discarded)
1277
+ node.unload() # unload web page
1283
1278
  self._stream_reset(pid)
1284
1279
  self.pids[pid].clear(all=True)
1285
1280
  self.pids[pid].loaded = False
@@ -1294,9 +1289,8 @@ class Renderer(BaseRenderer):
1294
1289
  :param meta: context meta
1295
1290
  """
1296
1291
  tab = node.get_tab()
1297
- tab.delete_ref(node)
1298
1292
  layout = tab.child.layout()
1299
- layout.removeWidget(node)
1293
+ tab.child.remove_widget(node)
1300
1294
  self.window.ui.nodes['output'].pop(tab.pid, None)
1301
1295
 
1302
1296
  node.on_delete()
@@ -1307,7 +1301,7 @@ class Renderer(BaseRenderer):
1307
1301
  view.signals.save_as.connect(self.window.controller.chat.render.handle_save_as)
1308
1302
  view.signals.audio_read.connect(self.window.controller.chat.render.handle_audio_read)
1309
1303
 
1310
- layout.addWidget(view)
1304
+ layout.addWidget(view) # tab body layout
1311
1305
  view.setVisible(True)
1312
1306
  self.window.ui.nodes['output'][tab.pid] = view
1313
1307
  try:
@@ -6,20 +6,18 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.24 23:00:00 #
9
+ # Updated Date: 2025.09.16 02:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import uuid
13
13
  from datetime import datetime
14
14
  from typing import Optional, Any, Dict, Tuple, Union
15
15
 
16
- from PySide6.QtCore import QUrl
17
16
  from PySide6.QtGui import QIcon
18
- from PySide6.QtWebEngineCore import QWebEnginePage
19
17
  from PySide6.QtWidgets import QVBoxLayout, QWidget, QLayout
20
18
 
21
19
  from pygpt_net.ui.widget.tabs.body import TabBody
22
- from pygpt_net.utils import trans
20
+ from pygpt_net.utils import trans, mem_clean
23
21
 
24
22
  from .tab import Tab
25
23
 
@@ -301,31 +299,26 @@ class Tabs:
301
299
  if tab.type == Tab.TAB_CHAT:
302
300
  node = self.window.ui.nodes['output'].get(tab.pid)
303
301
  if node:
304
- node.hide()
305
- p = node.page()
306
- p.triggerAction(QWebEnginePage.Stop)
307
- p.setUrl(QUrl("about:blank"))
308
- p.history().clear()
309
- p.setLifecycleState(QWebEnginePage.LifecycleState.Discarded)
310
- tab.delete_ref(node)
311
- layout = tab.child.layout()
312
- layout.removeWidget(node)
302
+ node.unload() # unload web page
303
+ tab.child.remove_widget(node)
313
304
  self.window.ui.nodes['output'].pop(pid, None)
314
305
  node.on_delete()
315
306
  node_plain = self.window.ui.nodes['output_plain'].get(tab.pid)
316
307
  if node_plain:
317
- tab.delete_ref(node_plain)
318
- layout = tab.child.layout()
319
- layout.removeWidget(node_plain)
308
+ tab.child.remove_widget(node_plain)
320
309
  self.window.ui.nodes['output_plain'].pop(pid, None)
321
310
  node_plain.on_delete()
322
311
 
323
312
  if tab.type in (Tab.TAB_CHAT, Tab.TAB_NOTEPAD, Tab.TAB_TOOL):
324
313
  tab.cleanup() # unload assigned data from memory
325
314
 
315
+ # tab.delete_refs()
316
+
326
317
  except Exception as e:
327
318
  print(f"Error unloading tab {pid}: {e}")
319
+ self.window.core.debug.log(e)
328
320
 
321
+ mem_clean(force=True)
329
322
  column_idx = tab.column_idx
330
323
  self.window.ui.layout.get_tabs_by_idx(column_idx).removeTab(tab.idx)
331
324
  del self.pids[pid]
@@ -333,7 +326,7 @@ class Tabs:
333
326
 
334
327
  def remove_all(self):
335
328
  """Remove all tabs"""
336
- for pid in list(self.pids):
329
+ for pid in list(self.pids.keys()):
337
330
  self.remove(pid) # delete from PIDs and UI
338
331
  self.pids = {}
339
332
  self.window.core.ctx.output.clear() # clear mapping
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "__meta__": {
3
- "version": "2.6.48",
4
- "app.version": "2.6.48",
5
- "updated_at": "2025-09-15T00:00:00"
3
+ "version": "2.6.50",
4
+ "app.version": "2.6.50",
5
+ "updated_at": "2025-09-16T00:00:00"
6
6
  },
7
7
  "access.audio.event.speech": false,
8
8
  "access.audio.event.speech.disabled": [],
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "__meta__": {
3
- "version": "2.6.48",
4
- "app.version": "2.6.48",
5
- "updated_at": "2025-09-15T08:03:34"
3
+ "version": "2.6.50",
4
+ "app.version": "2.6.50",
5
+ "updated_at": "2025-09-16T08:03:34"
6
6
  },
7
7
  "items": {
8
8
  "SpeakLeash/bielik-11b-v2.3-instruct:Q4_K_M": {
@@ -6,6 +6,27 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent lobortis lorem
6
6
  b = 2
7
7
  c = a + b
8
8
  print(c)
9
+ class ChatWebOutput(QWebEngineView):
10
+ def __init__(self, window=None):
11
+ """
12
+ HTML output (WebEngine)
13
+
14
+ :param window: Window instance
15
+ """
16
+ super(ChatWebOutput, self).__init__(window)
17
+ self.window = window
18
+ self.finder = WebFinder(window, self)
19
+ self.loadFinished.connect(self.on_page_loaded)
20
+ self.customContextMenuRequested.connect(self.on_context_menu)
21
+ self.signals = WebEngineSignals(self)
22
+ self.setContextMenuPolicy(Qt.CustomContextMenu)
23
+ self.filter = FocusEventFilter(self, self.on_focus)
24
+ self.installEventFilter(self)
25
+ self.plain = None
26
+ self.html_content = None
27
+ self.meta = None
28
+ self.tab = None
29
+ self.setProperty('class', 'layout-output-web')
9
30
  </execute>
10
31
 
11
32
  <tool>{"cmd": "fake_cmd", "params": {"foo": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."}}</tool>
pygpt_net/data/js/app.js CHANGED
@@ -1383,6 +1383,7 @@
1383
1383
  const rules = this.__compiled;
1384
1384
  if (!rules || !rules.length) return s;
1385
1385
 
1386
+ // Candidates: rules that want source-phase replacements (``` fences)
1386
1387
  const candidates = [];
1387
1388
  for (let i = 0; i < rules.length; i++) {
1388
1389
  const r = rules[i];
@@ -1391,27 +1392,42 @@
1391
1392
  }
1392
1393
  if (!candidates.length) return s;
1393
1394
 
1395
+ // Avoid touching content already inside Markdown code fences
1394
1396
  const fences = this._findFenceRanges(s);
1397
+
1398
+ let result = '';
1395
1399
  if (!fences.length) {
1396
- return this._applySourceReplacementsInChunk(s, s, 0, candidates);
1400
+ result = this._applySourceReplacementsInChunk(s, s, 0, candidates);
1401
+ } else {
1402
+ let out = '';
1403
+ let last = 0;
1404
+ for (let k = 0; k < fences.length; k++) {
1405
+ const [a, b] = fences[k];
1406
+ if (a > last) {
1407
+ const chunk = s.slice(last, a);
1408
+ out += this._applySourceReplacementsInChunk(s, chunk, last, candidates);
1409
+ }
1410
+ out += s.slice(a, b);
1411
+ last = b;
1412
+ }
1413
+ if (last < s.length) {
1414
+ const tail = s.slice(last);
1415
+ out += this._applySourceReplacementsInChunk(s, tail, last, candidates);
1416
+ }
1417
+ result = out;
1397
1418
  }
1398
1419
 
1399
- let out = '';
1400
- let last = 0;
1401
- for (let k = 0; k < fences.length; k++) {
1402
- const [a, b] = fences[k];
1403
- if (a > last) {
1404
- const chunk = s.slice(last, a);
1405
- out += this._applySourceReplacementsInChunk(s, chunk, last, candidates);
1420
+ // NEW: streaming-aware partial injection for unmatched source-fence openers.
1421
+ // If we are streaming and see a last opener without a closer, convert it to its openReplace
1422
+ // (e.g., <execute> -> ```python\n) so the snapshot immediately materializes a code block.
1423
+ if (opts && opts.streaming === true) {
1424
+ const fenceRules = candidates.filter(r => !!r.isSourceFence);
1425
+ if (fenceRules.length) {
1426
+ result = this._injectUnmatchedSourceOpeners(result, fenceRules);
1406
1427
  }
1407
- out += s.slice(a, b);
1408
- last = b;
1409
1428
  }
1410
- if (last < s.length) {
1411
- const tail = s.slice(last);
1412
- out += this._applySourceReplacementsInChunk(s, tail, last, candidates);
1413
- }
1414
- return out;
1429
+
1430
+ return result;
1415
1431
  }
1416
1432
 
1417
1433
  getSourceFenceSpecs() {
@@ -1996,6 +2012,44 @@
1996
2012
  }
1997
2013
  return t;
1998
2014
  }
2015
+
2016
+ // === NEW: streaming helper for unmatched source-fence openers ===
2017
+ _injectUnmatchedSourceOpeners(text, fenceRules) {
2018
+ // Production-grade guardrails
2019
+ let s = String(text || '');
2020
+ if (!s || !fenceRules || !fenceRules.length) return s;
2021
+
2022
+ // Find the last opener (closest to the end) that has no matching closer after it
2023
+ let best = null; // { r, idx }
2024
+ for (let i = 0; i < fenceRules.length; i++) {
2025
+ const r = fenceRules[i];
2026
+ if (!r || !r.open || !r.close || !r.openReplace) continue;
2027
+
2028
+ const idx = s.lastIndexOf(r.open);
2029
+ if (idx === -1) continue;
2030
+
2031
+ // Must be top-level line (so that ``` can start a fenced block)
2032
+ if (!this._isTopLevelLineInSource(s, idx)) continue;
2033
+
2034
+ // Ensure there is no closer after this opener in the current snapshot
2035
+ const after = s.indexOf(r.close, idx + r.open.length);
2036
+ if (after !== -1) continue;
2037
+
2038
+ if (!best || idx > best.idx) best = { r, idx };
2039
+ }
2040
+
2041
+ if (!best) return s;
2042
+
2043
+ const r = best.r;
2044
+ const i = best.idx;
2045
+
2046
+ // Replace the raw opener token with its source-fence open replacement (e.g. ```python\n)
2047
+ // This is ephemeral per-snapshot; underlying buffer remains unchanged.
2048
+ const before = s.slice(0, i);
2049
+ const after = s.slice(i + r.open.length);
2050
+ const injected = String(r.openReplace || '');
2051
+ return before + injected + after;
2052
+ }
1999
2053
  }
2000
2054
 
2001
2055
  // ==========================================================================