pygpt-net 2.6.33__py3-none-any.whl → 2.6.35__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. pygpt_net/CHANGELOG.txt +14 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/assistant/batch.py +14 -4
  4. pygpt_net/controller/assistant/files.py +1 -0
  5. pygpt_net/controller/assistant/store.py +195 -1
  6. pygpt_net/controller/camera/camera.py +1 -1
  7. pygpt_net/controller/chat/common.py +58 -48
  8. pygpt_net/controller/chat/handler/stream_worker.py +55 -43
  9. pygpt_net/controller/config/placeholder.py +95 -75
  10. pygpt_net/controller/dialogs/confirm.py +3 -1
  11. pygpt_net/controller/media/media.py +11 -3
  12. pygpt_net/controller/painter/common.py +243 -13
  13. pygpt_net/controller/painter/painter.py +11 -2
  14. pygpt_net/core/assistants/files.py +18 -0
  15. pygpt_net/core/bridge/bridge.py +1 -5
  16. pygpt_net/core/bridge/context.py +81 -36
  17. pygpt_net/core/bridge/worker.py +3 -1
  18. pygpt_net/core/camera/camera.py +31 -402
  19. pygpt_net/core/camera/worker.py +430 -0
  20. pygpt_net/core/ctx/bag.py +4 -0
  21. pygpt_net/core/events/app.py +10 -17
  22. pygpt_net/core/events/base.py +17 -25
  23. pygpt_net/core/events/control.py +9 -17
  24. pygpt_net/core/events/event.py +9 -62
  25. pygpt_net/core/events/kernel.py +8 -17
  26. pygpt_net/core/events/realtime.py +8 -17
  27. pygpt_net/core/events/render.py +9 -17
  28. pygpt_net/core/filesystem/url.py +3 -0
  29. pygpt_net/core/render/web/body.py +454 -40
  30. pygpt_net/core/render/web/pid.py +39 -24
  31. pygpt_net/core/render/web/renderer.py +146 -40
  32. pygpt_net/core/text/utils.py +3 -0
  33. pygpt_net/data/config/config.json +4 -3
  34. pygpt_net/data/config/models.json +3 -3
  35. pygpt_net/data/config/settings.json +10 -5
  36. pygpt_net/data/css/web-blocks.css +3 -2
  37. pygpt_net/data/css/web-chatgpt.css +3 -1
  38. pygpt_net/data/css/web-chatgpt_wide.css +3 -1
  39. pygpt_net/data/locale/locale.de.ini +9 -7
  40. pygpt_net/data/locale/locale.en.ini +10 -6
  41. pygpt_net/data/locale/locale.es.ini +9 -7
  42. pygpt_net/data/locale/locale.fr.ini +9 -7
  43. pygpt_net/data/locale/locale.it.ini +9 -7
  44. pygpt_net/data/locale/locale.pl.ini +9 -7
  45. pygpt_net/data/locale/locale.uk.ini +9 -7
  46. pygpt_net/data/locale/locale.zh.ini +9 -7
  47. pygpt_net/item/assistant.py +13 -1
  48. pygpt_net/provider/api/google/__init__.py +46 -28
  49. pygpt_net/provider/api/openai/__init__.py +13 -10
  50. pygpt_net/provider/api/openai/store.py +45 -1
  51. pygpt_net/provider/core/config/patch.py +9 -0
  52. pygpt_net/provider/llms/google.py +4 -0
  53. pygpt_net/ui/dialog/assistant_store.py +213 -203
  54. pygpt_net/ui/layout/chat/input.py +3 -3
  55. pygpt_net/ui/layout/chat/painter.py +63 -4
  56. pygpt_net/ui/widget/draw/painter.py +715 -104
  57. pygpt_net/ui/widget/option/combo.py +5 -1
  58. pygpt_net/ui/widget/textarea/input.py +273 -3
  59. pygpt_net/ui/widget/textarea/web.py +2 -0
  60. {pygpt_net-2.6.33.dist-info → pygpt_net-2.6.35.dist-info}/METADATA +16 -2
  61. {pygpt_net-2.6.33.dist-info → pygpt_net-2.6.35.dist-info}/RECORD +64 -63
  62. {pygpt_net-2.6.33.dist-info → pygpt_net-2.6.35.dist-info}/LICENSE +0 -0
  63. {pygpt_net-2.6.33.dist-info → pygpt_net-2.6.35.dist-info}/WHEEL +0 -0
  64. {pygpt_net-2.6.33.dist-info → pygpt_net-2.6.35.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,430 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ================================================== #
4
+ # This file is a part of PYGPT package #
5
+ # Website: https://pygpt.net #
6
+ # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
+ # MIT License #
8
+ # Created By : Marcin Szczygliński #
9
+ # Updated Date: 2025.09.02 16:00:00 #
10
+ # ================================================== #
11
+
12
+ import time
13
+
14
+ from PySide6.QtCore import QObject, Signal, QRunnable, Slot, QEventLoop, QTimer, Qt
15
+ from PySide6.QtGui import QImage
16
+
17
+ class CaptureSignals(QObject):
18
+ finished = Signal()
19
+ unfinished = Signal()
20
+ destroyed = Signal()
21
+ started = Signal()
22
+ stopped = Signal()
23
+ capture = Signal(object)
24
+ error = Signal(object)
25
+
26
+
27
+ class CaptureWorker(QRunnable):
28
+ def __init__(self, *args, **kwargs):
29
+ super().__init__()
30
+ self.signals = CaptureSignals()
31
+ self.args = args
32
+ self.kwargs = kwargs
33
+ self.window = None
34
+
35
+ # Common
36
+ self.initialized = False
37
+ self.allow_finish = False
38
+ self._fps_interval = 1.0 / 30.0 # default 30 FPS throttle
39
+
40
+ # Qt Multimedia objects (created in worker thread)
41
+ self.session = None
42
+ self.camera = None
43
+ self.sink = None
44
+ self.loop = None
45
+ self.poll_timer = None
46
+ self._qt_got_first_frame = False
47
+ self._probe_loop = None
48
+
49
+ # OpenCV fallback
50
+ self.cv_cap = None
51
+
52
+ # Timing (shared)
53
+ self._last_emit = 0.0
54
+
55
+ # =========================
56
+ # Qt Multimedia path
57
+ # =========================
58
+ def _select_camera_format(self, device, target_w: int, target_h: int):
59
+ """
60
+ Select best matching camera format by resolution.
61
+
62
+ :param device: QCameraDevice
63
+ :param target_w: target width
64
+ :param target_h: target height
65
+ """
66
+ try:
67
+ formats = list(device.videoFormats())
68
+ except Exception:
69
+ formats = []
70
+ if not formats:
71
+ return None
72
+
73
+ best = None
74
+ best_score = float('inf')
75
+ for f in formats:
76
+ res = f.resolution()
77
+ w, h = res.width(), res.height()
78
+ score = abs(w - target_w) + abs(h - target_h)
79
+ if score < best_score:
80
+ best_score = score
81
+ best = f
82
+ return best
83
+
84
+ def _init_qt(self) -> bool:
85
+ """
86
+ Try to initialize Qt camera pipeline.
87
+
88
+ :return: True if initialized
89
+ """
90
+ try:
91
+ from PySide6.QtMultimedia import (
92
+ QCamera,
93
+ QMediaDevices,
94
+ QMediaCaptureSession,
95
+ QVideoSink,
96
+ )
97
+
98
+ idx = int(self.window.core.config.get('vision.capture.idx') or 0)
99
+ target_w = int(self.window.core.config.get('vision.capture.width'))
100
+ target_h = int(self.window.core.config.get('vision.capture.height'))
101
+ target_fps = 30
102
+ self._fps_interval = 1.0 / float(target_fps)
103
+
104
+ devices = list(QMediaDevices.videoInputs())
105
+ if not devices:
106
+ return False
107
+
108
+ if idx < 0 or idx >= len(devices):
109
+ idx = 0
110
+ dev = devices[idx]
111
+
112
+ self.camera = QCamera(dev)
113
+ fmt = self._select_camera_format(dev, target_w, target_h)
114
+ if fmt is not None:
115
+ self.camera.setCameraFormat(fmt)
116
+
117
+ self.session = QMediaCaptureSession()
118
+ self.session.setCamera(self.camera)
119
+
120
+ self.sink = QVideoSink()
121
+ self.sink.videoFrameChanged.connect(self.on_qt_frame_changed, Qt.DirectConnection)
122
+ self.session.setVideoOutput(self.sink)
123
+
124
+ self.camera.errorOccurred.connect(self._on_qt_camera_error, Qt.QueuedConnection)
125
+ return True
126
+
127
+ except Exception as e:
128
+ # Qt Multimedia not available or failed to init
129
+ self.window.core.debug.log(e)
130
+ return False
131
+
132
+ def _teardown_qt(self):
133
+ """Release Qt camera pipeline."""
134
+ try:
135
+ if self.sink is not None:
136
+ try:
137
+ self.sink.videoFrameChanged.disconnect(self.on_qt_frame_changed)
138
+ except Exception:
139
+ pass
140
+ if self.camera is not None and self.camera.isActive():
141
+ self.camera.stop()
142
+ except Exception:
143
+ pass
144
+ finally:
145
+ self.sink = None
146
+ self.session = None
147
+ self.camera = None
148
+
149
+ def _probe_qt_start(self, timeout_ms: int = 1500) -> bool:
150
+ """
151
+ Wait briefly for the first frame to confirm Qt pipeline is working.
152
+
153
+ :param timeout_ms: timeout in milliseconds
154
+ :return: True if first frame received
155
+ """
156
+ try:
157
+ if self.camera is None:
158
+ return False
159
+
160
+ self._qt_got_first_frame = False
161
+ self._probe_loop = QEventLoop()
162
+
163
+ # Timeout quits the probe loop
164
+ QTimer.singleShot(timeout_ms, self._probe_loop.quit)
165
+
166
+ # Start camera and wait for first frame or timeout
167
+ self.camera.start()
168
+ self._probe_loop.exec()
169
+
170
+ got = self._qt_got_first_frame
171
+ self._probe_loop = None
172
+ return got
173
+ except Exception as e:
174
+ self.window.core.debug.log(e)
175
+ return False
176
+
177
+ @Slot(object)
178
+ def _on_qt_camera_error(self, err):
179
+ """
180
+ Handle Qt camera errors.
181
+
182
+ :param err: error object
183
+ """
184
+ try:
185
+ # Stop loop if running
186
+ if self.loop is not None and self.loop.isRunning():
187
+ self.loop.quit()
188
+ if self._probe_loop is not None and self._probe_loop.isRunning():
189
+ self._probe_loop.quit()
190
+ except Exception:
191
+ pass
192
+ finally:
193
+ self.allow_finish = False
194
+ if self.signals is not None:
195
+ self.signals.error.emit(err)
196
+
197
+ @Slot(object)
198
+ def on_qt_frame_changed(self, video_frame):
199
+ """
200
+ Convert QVideoFrame to RGB numpy array and emit.
201
+
202
+ :param video_frame: QVideoFrame
203
+ """
204
+ try:
205
+ # Mark that we have a first frame for probe
206
+ if not self._qt_got_first_frame:
207
+ self._qt_got_first_frame = True
208
+ # If we are probing, quit the probe loop immediately
209
+ if self._probe_loop is not None and self._probe_loop.isRunning():
210
+ self._probe_loop.quit()
211
+
212
+ # Throttle FPS for normal operation path
213
+ now = time.monotonic()
214
+ if self.loop is not None and self.loop.isRunning():
215
+ if (now - self._last_emit) < self._fps_interval:
216
+ return
217
+
218
+ img = video_frame.toImage()
219
+ if img.isNull():
220
+ return
221
+
222
+ img = img.convertToFormat(QImage.Format.Format_RGB888)
223
+
224
+ w = img.width()
225
+ h = img.height()
226
+ bpl = img.bytesPerLine()
227
+
228
+ ptr = img.bits()
229
+ size = bpl * h
230
+ try:
231
+ ptr.setsize(size)
232
+ except Exception:
233
+ # Some bindings may not require setsize; ignore if unsupported
234
+ pass
235
+
236
+ import numpy as np
237
+ arr = np.frombuffer(ptr, dtype=np.uint8)
238
+
239
+ if bpl != w * 3:
240
+ arr = arr.reshape(h, bpl)[:, : w * 3]
241
+ arr = arr.reshape(h, w, 3).copy()
242
+ else:
243
+ arr = arr.reshape(h, w, 3).copy()
244
+
245
+ if self.signals is not None:
246
+ self.signals.capture.emit(arr)
247
+ self._last_emit = now
248
+
249
+ except Exception as e:
250
+ self.window.core.debug.log(e)
251
+
252
+ # =========================
253
+ # OpenCV fallback path
254
+ # =========================
255
+ def _init_cv2(self) -> bool:
256
+ """
257
+ Try to initialize OpenCV VideoCapture fallback.
258
+
259
+ :return: True if initialized
260
+ """
261
+ try:
262
+ import cv2
263
+ idx = int(self.window.core.config.get('vision.capture.idx'))
264
+ target_w = int(self.window.core.config.get('vision.capture.width'))
265
+ target_h = int(self.window.core.config.get('vision.capture.height'))
266
+ target_fps = 30
267
+ self._fps_interval = 1.0 / float(target_fps)
268
+
269
+ cap = cv2.VideoCapture(idx)
270
+ if not cap or not cap.isOpened():
271
+ return False
272
+
273
+ cap.set(cv2.CAP_PROP_FRAME_WIDTH, target_w)
274
+ cap.set(cv2.CAP_PROP_FRAME_HEIGHT, target_h)
275
+ self.cv_cap = cap
276
+ return True
277
+ except Exception as e:
278
+ self.window.core.debug.log(e)
279
+ return False
280
+
281
+ def _teardown_cv2(self):
282
+ """Release OpenCV capture."""
283
+ try:
284
+ if self.cv_cap is not None and self.cv_cap.isOpened():
285
+ self.cv_cap.release()
286
+ except Exception:
287
+ pass
288
+ finally:
289
+ self.cv_cap = None
290
+
291
+ # =========================
292
+ # Runner
293
+ # =========================
294
+ @Slot()
295
+ def run(self):
296
+ """Run capture using Qt first; fall back to OpenCV if needed."""
297
+ self.allow_finish = True
298
+ self._last_emit = 0.0
299
+
300
+ used_backend = None
301
+ try:
302
+ # Try Qt Multimedia
303
+ if self._init_qt():
304
+ if self._probe_qt_start(timeout_ms=1500):
305
+ # Qt confirmed working; start main event-driven loop
306
+ used_backend = 'qt'
307
+ self.initialized = True
308
+ if self.signals is not None:
309
+ self.signals.started.emit()
310
+
311
+ self.loop = QEventLoop()
312
+
313
+ self.poll_timer = QTimer()
314
+ self.poll_timer.setTimerType(Qt.PreciseTimer)
315
+ self.poll_timer.setInterval(30)
316
+ self.poll_timer.timeout.connect(self._poll_stop_qt, Qt.DirectConnection)
317
+ self.poll_timer.start()
318
+
319
+ self.loop.exec()
320
+
321
+ if self.signals is not None:
322
+ self.signals.stopped.emit()
323
+ else:
324
+ # Fallback to OpenCV if no frames arrive quickly
325
+ print("QT camera init failed, trying CV2 fallback...")
326
+ self._teardown_qt()
327
+ else:
328
+ # Qt init failed outright, fallback to CV2
329
+ print("QT camera init failed, trying CV2 fallback...")
330
+
331
+ # Try OpenCV fallback if Qt was not used
332
+ if used_backend is None:
333
+ if self._init_cv2():
334
+ used_backend = 'cv2'
335
+ self.initialized = True
336
+ if self.signals is not None:
337
+ self.signals.started.emit()
338
+
339
+ import cv2
340
+ target_fps = 30
341
+ fps_interval = 1.0 / float(target_fps)
342
+ last_frame_time = time.time()
343
+
344
+ while True:
345
+ if self._should_stop():
346
+ break
347
+
348
+ ok, frame = self.cv_cap.read()
349
+ if not ok or frame is None:
350
+ continue
351
+
352
+ now = time.time()
353
+ if now - last_frame_time >= fps_interval:
354
+ # Convert BGR -> RGB for the controller/UI pipeline
355
+ frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
356
+ if self.signals is not None:
357
+ self.signals.capture.emit(frame)
358
+ last_frame_time = now
359
+
360
+ if self.signals is not None:
361
+ self.signals.stopped.emit()
362
+ else:
363
+ # Both providers failed
364
+ self.allow_finish = False
365
+
366
+ except Exception as e:
367
+ self.window.core.debug.log(e)
368
+ if self.signals is not None:
369
+ self.signals.error.emit(e)
370
+ finally:
371
+ # Cleanup resources
372
+ try:
373
+ if self.poll_timer is not None:
374
+ self.poll_timer.stop()
375
+ except Exception:
376
+ pass
377
+ self.poll_timer = None
378
+
379
+ if used_backend == 'qt':
380
+ self._teardown_qt()
381
+ else:
382
+ self._teardown_qt() # no-op if not initialized
383
+ self._teardown_cv2()
384
+
385
+ # Emit final state
386
+ if self.signals is not None:
387
+ if self.allow_finish:
388
+ self.signals.finished.emit()
389
+ else:
390
+ self.signals.unfinished.emit()
391
+
392
+ self.cleanup()
393
+
394
+ def _poll_stop_qt(self):
395
+ """Check stop flags while running Qt pipeline."""
396
+ try:
397
+ if self._should_stop():
398
+ if self.camera is not None and self.camera.isActive():
399
+ self.camera.stop()
400
+ if self.loop is not None and self.loop.isRunning():
401
+ self.loop.quit()
402
+ except Exception as e:
403
+ self.window.core.debug.log(e)
404
+ if self.loop is not None and self.loop.isRunning():
405
+ self.loop.quit()
406
+
407
+ def _should_stop(self) -> bool:
408
+ """
409
+ Check external stop flags.
410
+
411
+ :return: True if should stop
412
+ """
413
+ try:
414
+ if getattr(self.window, 'is_closing', False):
415
+ return True
416
+ if self.window is not None and self.window.controller.camera.stop:
417
+ return True
418
+ except Exception:
419
+ return True
420
+ return False
421
+
422
+ def cleanup(self):
423
+ """Cleanup resources after worker execution."""
424
+ sig = self.signals
425
+ self.signals = None
426
+ try:
427
+ if sig is not None:
428
+ sig.deleteLater()
429
+ except RuntimeError:
430
+ pass
pygpt_net/core/ctx/bag.py CHANGED
@@ -15,9 +15,13 @@ from pygpt_net.item.ctx import CtxItem
15
15
 
16
16
 
17
17
  class Bag:
18
+ __slots__ = ('window', 'meta', 'tab_id', 'items')
19
+
18
20
  def __init__(self, window=None):
19
21
  """
20
22
  Context bag
23
+
24
+ :param window: Window instance
21
25
  """
22
26
  self.window = window
23
27
  self.meta = None # current meta
@@ -6,15 +6,22 @@
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.23 21:00:00 #
9
+ # Updated Date: 2025.09.04 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from typing import Optional
12
+ from dataclasses import dataclass
13
+ from typing import Optional, Dict, Any, ClassVar
13
14
 
14
15
  from .base import BaseEvent
16
+ from ...item.ctx import CtxItem
15
17
 
18
+
19
+ @dataclass(slots=True)
16
20
  class AppEvent(BaseEvent):
17
21
  """Events dispatched by application"""
22
+ # static id for event family
23
+ id: ClassVar[str] = "AppEvent"
24
+
18
25
  APP_STARTED = "app.started"
19
26
  CAMERA_CAPTURED = "camera.captured"
20
27
  CAMERA_DISABLED = "camera.disabled"
@@ -37,18 +44,4 @@ class AppEvent(BaseEvent):
37
44
  VOICE_CONTROL_STOPPED = "voice.control.stopped"
38
45
  VOICE_CONTROL_SENT = "voice.control.sent"
39
46
  VOICE_CONTROL_TOGGLE = "voice.control.toggle"
40
- VOICE_CONTROL_UNRECOGNIZED = "voice.control.unrecognized"
41
-
42
- def __init__(
43
- self,
44
- name: Optional[str] = None,
45
- data: Optional[dict] = None,
46
- ):
47
- """
48
- Event object class
49
-
50
- :param name: event name
51
- :param data: event data
52
- """
53
- super(AppEvent, self).__init__(name, data)
54
- self.id = "AppEvent"
47
+ VOICE_CONTROL_UNRECOGNIZED = "voice.control.unrecognized"
@@ -6,39 +6,31 @@
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.06 19:00:00 #
9
+ # Updated Date: 2025.09.04 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import json
13
- from typing import Optional, Dict, Any
13
+ from dataclasses import dataclass
14
+ from typing import Optional, Dict, Any, ClassVar
14
15
 
15
16
  from pygpt_net.item.ctx import CtxItem
16
17
 
17
- class BaseEvent:
18
18
 
19
- def __init__(
20
- self,
21
- name: Optional[str] = None,
22
- data: Optional[dict] = None,
23
- ctx: CtxItem = None
24
- ):
25
- """
26
- Base Event object class
19
+ @dataclass(slots=True)
20
+ class BaseEvent:
21
+ """Base Event object class"""
22
+ id: ClassVar[str] = None
23
+ name: Optional[str] = None
24
+ data: Optional[dict] = None
25
+ ctx: Optional[CtxItem] = None # CtxItem
26
+ stop: bool = False # True to stop propagation
27
+ internal: bool = False
28
+ call_id: int = 0
27
29
 
28
- :param name: event name
29
- :param data: event data
30
- :param ctx: context instance
31
- """
32
- self.id = None
33
- self.name = name
34
- self.data = data
30
+ def __post_init__(self):
31
+ # Normalize None to empty dict for convenience and safety
35
32
  if self.data is None:
36
33
  self.data = {}
37
- self.ctx = ctx # CtxItem
38
- self.stop = False # True to stop propagation
39
- self.internal = False
40
- self.call_id = 0
41
-
42
34
 
43
35
  @property
44
36
  def full_name(self) -> str:
@@ -47,7 +39,7 @@ class BaseEvent:
47
39
 
48
40
  :return: Full event name
49
41
  """
50
- return self.id + ": " + self.name
42
+ return self.id + ": " + self.name # type: ignore[operator]
51
43
 
52
44
  def to_dict(self) -> Dict[str, Any]:
53
45
  """Dump event to dict"""
@@ -66,7 +58,7 @@ class BaseEvent:
66
58
  """
67
59
  try:
68
60
  return json.dumps(self.to_dict())
69
- except Exception as e:
61
+ except Exception:
70
62
  pass
71
63
  return ""
72
64
 
@@ -6,16 +6,22 @@
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.23 21:00:00 #
9
+ # Updated Date: 2025.09.04 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from typing import Optional
12
+ from dataclasses import dataclass
13
+ from typing import Optional, ClassVar
13
14
 
14
15
  from .base import BaseEvent
16
+ from ...item.ctx import CtxItem
15
17
 
16
18
 
19
+ @dataclass(slots=True)
17
20
  class ControlEvent(BaseEvent):
18
21
  """Events used for app control"""
22
+ # static id for event family
23
+ id: ClassVar[str] = "ControlEvent"
24
+
19
25
  APP_EXIT = "app.exit"
20
26
  APP_STATUS = "app.status"
21
27
  AUDIO_INPUT_DISABLE = "audio.input.disable"
@@ -72,18 +78,4 @@ class ControlEvent(BaseEvent):
72
78
  VOICE_CONTROL_UNRECOGNIZED = "unrecognized"
73
79
  VOICE_MESSAGE_START = "voice_msg.start"
74
80
  VOICE_MESSAGE_STOP = "voice_msg.stop"
75
- VOICE_MESSAGE_TOGGLE = "voice_msg.toggle"
76
-
77
- def __init__(
78
- self,
79
- name: Optional[str] = None,
80
- data: Optional[dict] = None,
81
- ):
82
- """
83
- Event object class
84
-
85
- :param name: event name
86
- :param data: event data
87
- """
88
- super(ControlEvent, self).__init__(name, data)
89
- self.id = "ControlEvent"
81
+ VOICE_MESSAGE_TOGGLE = "voice_msg.toggle"
@@ -6,16 +6,22 @@
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.23 21:00:00 #
9
+ # Updated Date: 2025.09.04 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import json
13
- from typing import Optional, Dict, Any
13
+ from dataclasses import dataclass
14
+ from typing import Optional, Dict, Any, ClassVar
14
15
 
15
16
  from pygpt_net.item.ctx import CtxItem
16
17
  from .base import BaseEvent
17
18
 
19
+
20
+ @dataclass(slots=True)
18
21
  class Event(BaseEvent):
22
+ """Generic event with context serialization"""
23
+ # static id for event family
24
+ id: ClassVar[str] = "Event"
19
25
 
20
26
  # Events
21
27
  AI_NAME = "ai.name"
@@ -63,63 +69,4 @@ class Event(BaseEvent):
63
69
  UI_ATTACHMENTS = "ui.attachments"
64
70
  UI_VISION = "ui.vision"
65
71
  USER_NAME = "user.name"
66
- USER_SEND = "user.send"
67
-
68
- def __init__(
69
- self,
70
- name: Optional[str] = None,
71
- data: Optional[dict] = None,
72
- ctx: Optional[CtxItem] = None
73
- ):
74
- """
75
- Event object class
76
-
77
- :param name: event name
78
- :param data: event data
79
- :param ctx: context instance
80
- """
81
- super(Event, self).__init__(name, data, ctx)
82
- self.id = "Event"
83
- self.name = name
84
- self.data = data
85
- if self.data is None:
86
- self.data = {}
87
- self.ctx = ctx # CtxItem
88
- self.stop = False # True to stop propagation
89
- self.internal = False
90
- # internal event, not called from user
91
- # internal event is handled synchronously, ctx item has internal flag
92
-
93
- def to_dict(self) -> Dict[str, Any]:
94
- """
95
- Dump event to dict
96
-
97
- :return: Event dict
98
- """
99
- return {
100
- 'name': self.name,
101
- 'data': self.data,
102
- 'ctx': self.ctx.to_dict() if self.ctx else None,
103
- 'stop': self.stop,
104
- 'internal': self.internal,
105
- }
106
-
107
- def dump(self) -> str:
108
- """
109
- Dump event to json string
110
-
111
- :return: JSON string
112
- """
113
- try:
114
- return json.dumps(self.to_dict())
115
- except Exception as e:
116
- pass
117
- return ""
118
-
119
- def __str__(self) -> str:
120
- """
121
- String representation of event
122
-
123
- :return: Event string
124
- """
125
- return self.dump()
72
+ USER_SEND = "user.send"