pygpt-net 2.7.7__py3-none-any.whl → 2.7.9__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 (98) hide show
  1. pygpt_net/CHANGELOG.txt +12 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +5 -1
  4. pygpt_net/controller/assistant/batch.py +2 -2
  5. pygpt_net/controller/assistant/files.py +7 -6
  6. pygpt_net/controller/assistant/threads.py +0 -0
  7. pygpt_net/controller/chat/command.py +0 -0
  8. pygpt_net/controller/dialogs/confirm.py +35 -58
  9. pygpt_net/controller/lang/mapping.py +9 -9
  10. pygpt_net/controller/realtime/realtime.py +13 -1
  11. pygpt_net/controller/remote_store/{google/batch.py → batch.py} +209 -252
  12. pygpt_net/controller/remote_store/remote_store.py +982 -13
  13. pygpt_net/core/command/command.py +0 -0
  14. pygpt_net/core/db/viewer.py +1 -1
  15. pygpt_net/core/realtime/worker.py +3 -1
  16. pygpt_net/{controller/remote_store/google → core/remote_store/anthropic}/__init__.py +0 -1
  17. pygpt_net/core/remote_store/anthropic/files.py +211 -0
  18. pygpt_net/core/remote_store/anthropic/store.py +208 -0
  19. pygpt_net/core/remote_store/openai/store.py +5 -4
  20. pygpt_net/core/remote_store/remote_store.py +5 -1
  21. pygpt_net/{controller/remote_store/openai → core/remote_store/xai}/__init__.py +0 -1
  22. pygpt_net/core/remote_store/xai/files.py +225 -0
  23. pygpt_net/core/remote_store/xai/store.py +219 -0
  24. pygpt_net/data/config/config.json +10 -6
  25. pygpt_net/data/config/models.json +38 -22
  26. pygpt_net/data/config/settings.json +54 -1
  27. pygpt_net/data/icons/folder_eye.svg +1 -0
  28. pygpt_net/data/icons/folder_eye_filled.svg +1 -0
  29. pygpt_net/data/icons/folder_open.svg +1 -0
  30. pygpt_net/data/icons/folder_open_filled.svg +1 -0
  31. pygpt_net/data/locale/locale.de.ini +4 -3
  32. pygpt_net/data/locale/locale.en.ini +14 -4
  33. pygpt_net/data/locale/locale.es.ini +4 -3
  34. pygpt_net/data/locale/locale.fr.ini +4 -3
  35. pygpt_net/data/locale/locale.it.ini +4 -3
  36. pygpt_net/data/locale/locale.pl.ini +5 -4
  37. pygpt_net/data/locale/locale.uk.ini +4 -3
  38. pygpt_net/data/locale/locale.zh.ini +4 -3
  39. pygpt_net/icons.qrc +4 -0
  40. pygpt_net/icons_rc.py +282 -138
  41. pygpt_net/provider/api/anthropic/__init__.py +2 -0
  42. pygpt_net/provider/api/anthropic/chat.py +84 -1
  43. pygpt_net/provider/api/anthropic/store.py +307 -0
  44. pygpt_net/provider/api/anthropic/stream.py +75 -0
  45. pygpt_net/provider/api/anthropic/worker/__init__.py +0 -0
  46. pygpt_net/provider/api/anthropic/worker/importer.py +278 -0
  47. pygpt_net/provider/api/google/chat.py +59 -2
  48. pygpt_net/provider/api/google/realtime/client.py +70 -24
  49. pygpt_net/provider/api/google/realtime/realtime.py +48 -12
  50. pygpt_net/provider/api/google/store.py +124 -3
  51. pygpt_net/provider/api/google/stream.py +91 -24
  52. pygpt_net/provider/api/google/worker/importer.py +16 -28
  53. pygpt_net/provider/api/openai/assistants.py +2 -2
  54. pygpt_net/provider/api/openai/realtime/realtime.py +26 -6
  55. pygpt_net/provider/api/openai/store.py +4 -1
  56. pygpt_net/provider/api/openai/worker/importer.py +19 -61
  57. pygpt_net/provider/api/openai/worker/importer_assistants.py +230 -0
  58. pygpt_net/provider/api/x_ai/__init__.py +27 -6
  59. pygpt_net/provider/api/x_ai/audio.py +43 -11
  60. pygpt_net/provider/api/x_ai/chat.py +92 -4
  61. pygpt_net/provider/api/x_ai/realtime/__init__.py +12 -0
  62. pygpt_net/provider/api/x_ai/realtime/client.py +1864 -0
  63. pygpt_net/provider/api/x_ai/realtime/realtime.py +213 -0
  64. pygpt_net/provider/api/x_ai/remote_tools.py +102 -1
  65. pygpt_net/provider/api/x_ai/store.py +610 -0
  66. pygpt_net/provider/api/x_ai/stream.py +30 -9
  67. pygpt_net/provider/api/x_ai/tools.py +51 -0
  68. pygpt_net/provider/api/x_ai/worker/importer.py +308 -0
  69. pygpt_net/provider/audio_input/xai_grok_voice.py +390 -0
  70. pygpt_net/provider/audio_output/xai_tts.py +325 -0
  71. pygpt_net/provider/core/config/patch.py +29 -3
  72. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +2 -2
  73. pygpt_net/provider/core/model/patch.py +49 -1
  74. pygpt_net/tools/image_viewer/tool.py +334 -34
  75. pygpt_net/tools/image_viewer/ui/dialogs.py +317 -21
  76. pygpt_net/ui/dialog/assistant.py +1 -1
  77. pygpt_net/ui/dialog/plugins.py +13 -5
  78. pygpt_net/ui/dialog/remote_store.py +552 -0
  79. pygpt_net/ui/dialogs.py +3 -5
  80. pygpt_net/ui/layout/ctx/ctx_list.py +58 -7
  81. pygpt_net/ui/menu/tools.py +6 -13
  82. pygpt_net/ui/widget/dialog/{remote_store_google.py → remote_store.py} +10 -10
  83. pygpt_net/ui/widget/element/button.py +4 -4
  84. pygpt_net/ui/widget/image/display.py +2 -2
  85. pygpt_net/ui/widget/lists/context.py +2 -2
  86. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/METADATA +14 -2
  87. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/RECORD +87 -75
  88. pygpt_net/controller/remote_store/google/store.py +0 -615
  89. pygpt_net/controller/remote_store/openai/batch.py +0 -524
  90. pygpt_net/controller/remote_store/openai/store.py +0 -699
  91. pygpt_net/ui/dialog/remote_store_google.py +0 -539
  92. pygpt_net/ui/dialog/remote_store_openai.py +0 -539
  93. pygpt_net/ui/widget/dialog/remote_store_openai.py +0 -56
  94. pygpt_net/ui/widget/lists/remote_store_google.py +0 -248
  95. pygpt_net/ui/widget/lists/remote_store_openai.py +0 -317
  96. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/LICENSE +0 -0
  97. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/WHEEL +0 -0
  98. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.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: 2026.01.02 20:00:00 #
9
+ # Updated Date: 2026.01.06 06:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -33,18 +33,19 @@ class Importer(QObject):
33
33
  :param mode: mode
34
34
  :param err: error message
35
35
  """
36
+ batch = self.window.controller.remote_store.batch
36
37
  if mode == "import_files":
37
- self.window.controller.remote_store.google.batch.handle_imported_files_failed(err)
38
+ batch.handle_imported_files_failed(err)
38
39
  elif mode == "truncate_files":
39
- self.window.controller.remote_store.google.batch.handle_truncated_files_failed(err)
40
+ batch.handle_truncated_files_failed(err)
40
41
  elif mode == "upload_files":
41
- self.window.controller.remote_store.google.batch.handle_uploaded_files_failed(err)
42
+ batch.handle_uploaded_files_failed(err)
42
43
  elif mode in "vector_stores":
43
- self.window.controller.remote_store.google.batch.handle_imported_stores_failed(err)
44
+ batch.handle_imported_stores_failed(err)
44
45
  elif mode in "truncate_vector_stores":
45
- self.window.controller.remote_store.google.batch.handle_truncated_stores_failed(err)
46
+ batch.handle_truncated_stores_failed(err)
46
47
  elif mode in "refresh_vector_stores":
47
- self.window.controller.remote_store.google.batch.handle_refreshed_stores_failed(err)
48
+ batch.handle_refreshed_stores_failed(err)
48
49
 
49
50
  @Slot(str, str, int)
50
51
  def handle_finished(self, mode: str, store_id: str = None, num: int = 0):
@@ -55,18 +56,19 @@ class Importer(QObject):
55
56
  :param store_id: store ID
56
57
  :param num: number of affected items
57
58
  """
59
+ batch = self.window.controller.remote_store.batch
58
60
  if mode == "import_files":
59
- self.window.controller.remote_store.google.batch.handle_imported_files(num)
61
+ batch.handle_imported_files(num)
60
62
  elif mode == "truncate_files":
61
- self.window.controller.remote_store.google.batch.handle_truncated_files(store_id, num)
63
+ batch.handle_truncated_files(store_id, num)
62
64
  elif mode == "upload_files":
63
- self.window.controller.remote_store.google.batch.handle_uploaded_files(num)
65
+ batch.handle_uploaded_files(num)
64
66
  elif mode == "vector_stores":
65
- self.window.controller.remote_store.google.batch.handle_imported_stores(num)
67
+ batch.handle_imported_stores(num)
66
68
  elif mode == "truncate_vector_stores":
67
- self.window.controller.remote_store.google.batch.handle_truncated_stores(num)
69
+ batch.handle_truncated_stores(num)
68
70
  elif mode == "refresh_vector_stores":
69
- self.window.controller.remote_store.google.batch.handle_refreshed_stores(num)
71
+ batch.handle_refreshed_stores(num)
70
72
 
71
73
  @Slot(str, str)
72
74
  def handle_status(self, mode: str, msg: str):
@@ -216,20 +218,6 @@ class ImportWorker(QRunnable):
216
218
  finally:
217
219
  self.cleanup()
218
220
 
219
- def import_assistants(self, silent: bool = False) -> bool:
220
- """
221
- Import assistants (not used for Google by default; kept for parity)
222
-
223
- :param silent: silent mode
224
- """
225
- try:
226
- if not silent:
227
- self.signals.finished.emit("assistants", self.store_id, 0)
228
- return True
229
- except Exception as e:
230
- self.signals.error.emit("assistants", e)
231
- return False
232
-
233
221
  def import_vector_stores(self, silent: bool = False) -> bool:
234
222
  """
235
223
  Import File Search stores
@@ -282,7 +270,7 @@ class ImportWorker(QRunnable):
282
270
  for id in stores:
283
271
  store = stores[id]
284
272
  try:
285
- self.window.controller.remote_store.google.refresh_store(store, update=False)
273
+ self.window.controller.remote_store.refresh_store(store, update=False, provider="google")
286
274
  num += 1
287
275
  except Exception as e:
288
276
  self.log("Failed to refresh store: {}".format(id))
@@ -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: 2024.12.14 22:00:00 #
9
+ # Updated Date: 2026.01.05 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import json
@@ -16,7 +16,7 @@ from pygpt_net.item.assistant import AssistantItem
16
16
  from pygpt_net.item.ctx import CtxItem
17
17
 
18
18
  from .worker.assistants import AssistantsWorker, EventHandler
19
- from .worker.importer import Importer
19
+ from .worker.importer_assistants import Importer
20
20
 
21
21
 
22
22
  class Assistants:
@@ -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.01 23:00:00 #
9
+ # Updated Date: 2026.01.07 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import json
@@ -102,6 +102,31 @@ class Realtime:
102
102
  self.handler.update_ctx(context.ctx)
103
103
  return True # do not start new session, just send tool results
104
104
 
105
+ # Resolve last session ID from history only (do not fallback anywhere)
106
+ last_session_id = extract_last_session_id(context.history) if context.history else None
107
+ if is_debug:
108
+ print("[realtime session] Last ID", last_session_id)
109
+
110
+ # Enforce clean state rules before any live updates:
111
+ # - If there is no history at all: always reset live session to ensure a fresh context.
112
+ # - If there is history but it has no resumable session id: close any active session to avoid accidental continuation.
113
+ try:
114
+ history_len = len(context.history) if context.history else 0
115
+ except Exception:
116
+ history_len = 0
117
+
118
+ if history_len == 0:
119
+ if self.handler.is_session_active():
120
+ self.handler.close_session_sync()
121
+ try:
122
+ if context.ctx and isinstance(context.ctx.extra, dict):
123
+ context.ctx.extra.pop("rt_session_id", None)
124
+ except Exception:
125
+ pass
126
+ last_session_id = None # force new session
127
+ elif not last_session_id and self.handler.is_session_active():
128
+ self.handler.close_session_sync()
129
+
105
130
  # update auto-turn in active session
106
131
  if (self.handler.is_session_active()
107
132
  and (auto_turn != self.prev_auto_turn
@@ -116,11 +141,6 @@ class Realtime:
116
141
  self.window.update_status(trans("speech.listening"))
117
142
  return True # do not send new request if session is active
118
143
 
119
- # Last session ID
120
- last_session_id = extract_last_session_id(context.history)
121
- if is_debug:
122
- print("[realtime session] Last ID", last_session_id)
123
-
124
144
  # Voice
125
145
  voice = "alloy"
126
146
  try:
@@ -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: 2026.01.02 20:00:00 #
9
+ # Updated Date: 2026.01.05 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -14,6 +14,8 @@ from typing import Optional, List
14
14
 
15
15
  from pygpt_net.item.store import RemoteStoreItem
16
16
 
17
+ from .worker.importer import Importer
18
+
17
19
 
18
20
  class Store:
19
21
  def __init__(self, window=None):
@@ -23,6 +25,7 @@ class Store:
23
25
  :param window: Window instance
24
26
  """
25
27
  self.window = window
28
+ self.importer = Importer(window)
26
29
 
27
30
  def get_client(self):
28
31
  """
@@ -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: 2026.01.02 19:00:00 #
9
+ # Updated Date: 2026.01.05 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -33,20 +33,19 @@ class Importer(QObject):
33
33
  :param mode: mode
34
34
  :param err: error message
35
35
  """
36
- if mode == "assistants":
37
- self.window.controller.assistant.batch.handle_imported_assistants_failed(err)
38
- elif mode == "import_files":
39
- self.window.controller.remote_store.openai.batch.handle_imported_files_failed(err)
36
+ batch = self.window.controller.remote_store.batch
37
+ if mode == "import_files":
38
+ batch.handle_imported_files_failed(err)
40
39
  elif mode == "truncate_files":
41
- self.window.controller.remote_store.openai.batch.handle_truncated_files_failed(err)
40
+ batch.handle_truncated_files_failed(err)
42
41
  elif mode == "upload_files":
43
- self.window.controller.remote_store.openai.batch.handle_uploaded_files_failed(err)
42
+ batch.handle_uploaded_files_failed(err)
44
43
  elif mode in "vector_stores":
45
- self.window.controller.remote_store.openai.batch.handle_imported_stores_failed(err)
44
+ batch.handle_imported_stores_failed(err)
46
45
  elif mode in "truncate_vector_stores":
47
- self.window.controller.remote_store.openai.batch.handle_truncated_stores_failed(err)
46
+ batch.handle_truncated_stores_failed(err)
48
47
  elif mode in "refresh_vector_stores":
49
- self.window.controller.remote_store.openai.batch.handle_refreshed_stores_failed(err)
48
+ batch.handle_refreshed_stores_failed(err)
50
49
 
51
50
  @Slot(str, str, int)
52
51
  def handle_finished(self, mode: str, store_id: str = None, num: int = 0):
@@ -57,20 +56,19 @@ class Importer(QObject):
57
56
  :param store_id: store ID
58
57
  :param num: number of affected items
59
58
  """
60
- if mode == "assistants":
61
- self.window.controller.assistant.batch.handle_imported_assistants(num)
62
- elif mode == "import_files":
63
- self.window.controller.remote_store.openai.batch.handle_imported_files(num)
59
+ batch = self.window.controller.remote_store.batch
60
+ if mode == "import_files":
61
+ batch.handle_imported_files(num)
64
62
  elif mode == "truncate_files":
65
- self.window.controller.remote_store.openai.batch.handle_truncated_files(store_id, num)
63
+ batch.handle_truncated_files(store_id, num)
66
64
  elif mode == "upload_files":
67
- self.window.controller.remote_store.openai.batch.handle_uploaded_files(num)
65
+ batch.handle_uploaded_files(num)
68
66
  elif mode == "vector_stores":
69
- self.window.controller.remote_store.openai.batch.handle_imported_stores(num)
67
+ batch.handle_imported_stores(num)
70
68
  elif mode == "truncate_vector_stores":
71
- self.window.controller.remote_store.openai.batch.handle_truncated_stores(num)
69
+ batch.handle_truncated_stores(num)
72
70
  elif mode == "refresh_vector_stores":
73
- self.window.controller.remote_store.openai.batch.handle_refreshed_stores(num)
71
+ batch.handle_refreshed_stores(num)
74
72
 
75
73
  @Slot(str, str)
76
74
  def handle_status(self, mode: str, msg: str):
@@ -92,14 +90,6 @@ class Importer(QObject):
92
90
  """
93
91
  self.window.controller.assistant.threads.log(mode + ": " + msg)
94
92
 
95
- def import_assistants(self):
96
- """Import assistants"""
97
- self.worker = ImportWorker()
98
- self.worker.window = self.window
99
- self.worker.mode = "assistants"
100
- self.connect_signals(self.worker)
101
- self.window.threadpool.start(self.worker)
102
-
103
93
  def import_vector_stores(self):
104
94
  """Import vector stores"""
105
95
  self.worker = ImportWorker()
@@ -203,9 +193,7 @@ class ImportWorker(QRunnable):
203
193
  """Importer thread"""
204
194
  try:
205
195
  # import data
206
- if self.mode == "assistants":
207
- self.import_assistants()
208
- elif self.mode == "vector_stores":
196
+ if self.mode == "vector_stores":
209
197
  if self.import_vector_stores():
210
198
  self.import_files()
211
199
  elif self.mode == "truncate_vector_stores":
@@ -225,36 +213,6 @@ class ImportWorker(QRunnable):
225
213
  finally:
226
214
  self.cleanup()
227
215
 
228
- def import_assistants(self, silent: bool = False) -> bool:
229
- """
230
- Import assistants from API
231
-
232
- :param silent: silent mode (no signals)
233
- :return: result
234
- """
235
- try:
236
- # import assistants
237
- self.log("Importing assistants...")
238
- self.window.core.assistants.clear()
239
- items = self.window.core.assistants.get_all()
240
- self.window.core.api.openai.assistants.import_all(items, callback=self.callback)
241
- self.window.core.assistants.items = items
242
- self.window.core.assistants.save()
243
-
244
- # import vector stores
245
- self.import_vector_stores(True)
246
-
247
- # import files
248
- self.import_files(True)
249
-
250
- if not silent:
251
- self.signals.finished.emit("assistants", self.store_id, len(items))
252
- return True
253
- except Exception as e:
254
- self.log("API error: {}".format(e))
255
- self.signals.error.emit("assistants", e)
256
- return False
257
-
258
216
  def import_vector_stores(self, silent: bool = False) -> bool:
259
217
  """
260
218
  Import vector stores from API
@@ -310,7 +268,7 @@ class ImportWorker(QRunnable):
310
268
  for id in stores:
311
269
  store = stores[id]
312
270
  try:
313
- self.window.controller.remote_store.openai.refresh_store(store, update=False)
271
+ self.window.controller.remote_store.refresh_store(store, update=False, provider="openai")
314
272
  num += 1
315
273
  except Exception as e:
316
274
  self.log("Failed to refresh store: {}".format(id))
@@ -0,0 +1,230 @@
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: 2026.01.05 17:00:00 #
10
+ # ================================================== #
11
+
12
+ import os
13
+
14
+ from PySide6.QtCore import QObject, Signal, QRunnable, Slot
15
+
16
+
17
+ class Importer(QObject):
18
+ def __init__(self, window=None):
19
+ """
20
+ Importer core
21
+
22
+ :param window: Window instance
23
+ """
24
+ super(Importer, self).__init__()
25
+ self.window = window
26
+ self.worker = None
27
+
28
+ @Slot(str, object)
29
+ def handle_error(self, mode: str, err: any):
30
+ """
31
+ Handle thread error signal
32
+
33
+ :param mode: mode
34
+ :param err: error message
35
+ """
36
+ batch = self.window.controller.remote_store.batch
37
+ if mode == "assistants":
38
+ self.window.controller.assistant.batch.handle_imported_assistants_failed(err)
39
+
40
+ @Slot(str, str, int)
41
+ def handle_finished(self, mode: str, store_id: str = None, num: int = 0):
42
+ """
43
+ Handle thread finished signal
44
+
45
+ :param mode: mode
46
+ :param store_id: store ID
47
+ :param num: number of affected items
48
+ """
49
+ batch = self.window.controller.remote_store.batch
50
+ if mode == "assistants":
51
+ self.window.controller.assistant.batch.handle_imported_assistants(num)
52
+
53
+ @Slot(str, str)
54
+ def handle_status(self, mode: str, msg: str):
55
+ """
56
+ Handle thread status change signal
57
+
58
+ :param mode: mode
59
+ :param msg: message
60
+ """
61
+ self.window.controller.assistant.batch.handle_status_change(mode, msg)
62
+
63
+ @Slot(str, str)
64
+ def handle_log(self, mode: str, msg: str):
65
+ """
66
+ Handle thread log message signal
67
+
68
+ :param mode: mode
69
+ :param msg: message
70
+ """
71
+ self.window.controller.assistant.threads.log(mode + ": " + msg)
72
+
73
+ def import_assistants(self):
74
+ """Import assistants"""
75
+ self.worker = ImportWorker()
76
+ self.worker.window = self.window
77
+ self.worker.mode = "assistants"
78
+ self.connect_signals(self.worker)
79
+ self.window.threadpool.start(self.worker)
80
+
81
+ def connect_signals(self, worker):
82
+ """
83
+ Connect signals
84
+
85
+ :param worker: worker instance
86
+ """
87
+ worker.signals.finished.connect(self.handle_finished)
88
+ worker.signals.error.connect(self.handle_error)
89
+ worker.signals.status.connect(self.handle_status)
90
+ worker.signals.log.connect(self.handle_log)
91
+
92
+
93
+ class ImportWorkerSignals(QObject):
94
+ """Import worker signals"""
95
+ status = Signal(str, str) # mode, message
96
+ finished = Signal(str, str, int) # mode, store_id, num
97
+ error = Signal(str, object) # mode, error
98
+ log = Signal(str, str) # mode, message
99
+
100
+
101
+ class ImportWorker(QRunnable):
102
+ """Import worker"""
103
+ def __init__(self, *args, **kwargs):
104
+ super().__init__()
105
+ self.signals = ImportWorkerSignals()
106
+ self.window = None
107
+ self.mode = "assistants"
108
+ self.assistant = None
109
+ self.store_id = None
110
+ self.files = []
111
+
112
+ @Slot()
113
+ def run(self):
114
+ """Importer thread"""
115
+ try:
116
+ # import data
117
+ if self.mode == "assistants":
118
+ self.import_assistants()
119
+
120
+ except Exception as e:
121
+ self.signals.error.emit(self.mode, e)
122
+
123
+ finally:
124
+ self.cleanup()
125
+
126
+ def import_assistants(self, silent: bool = False) -> bool:
127
+ """
128
+ Import assistants from API
129
+
130
+ :param silent: silent mode (no signals)
131
+ :return: result
132
+ """
133
+ try:
134
+ # import assistants
135
+ self.log("Importing assistants...")
136
+ self.window.core.assistants.clear()
137
+ items = self.window.core.assistants.get_all()
138
+ self.window.core.api.openai.assistants.import_all(items, callback=self.callback)
139
+ self.window.core.assistants.items = items
140
+ self.window.core.assistants.save()
141
+
142
+ # import vector stores
143
+ self.import_vector_stores(True)
144
+
145
+ # import files
146
+ self.import_files(True)
147
+
148
+ if not silent:
149
+ self.signals.finished.emit("assistants", self.store_id, len(items))
150
+ return True
151
+ except Exception as e:
152
+ self.log("API error: {}".format(e))
153
+ self.signals.error.emit("assistants", e)
154
+ return False
155
+
156
+ def import_vector_stores(self, silent: bool = False) -> bool:
157
+ """
158
+ Import vector stores from API
159
+
160
+ :param silent: silent mode (no signals emit)
161
+ :return: result
162
+ """
163
+ try:
164
+ self.log("Importing vector stores...")
165
+ self.window.core.remote_store.openai.clear()
166
+ items = {}
167
+ self.window.core.api.openai.store.import_stores(items, callback=self.callback)
168
+ self.window.core.remote_store.openai.import_items(items)
169
+ if not silent:
170
+ self.signals.finished.emit("vector_stores", self.store_id, len(items))
171
+ return True
172
+ except Exception as e:
173
+ self.log("API error: {}".format(e))
174
+ self.signals.error.emit("vector_stores", e)
175
+ return False
176
+
177
+ def import_files(self, silent: bool = False) -> bool:
178
+ """
179
+ Import assistant files from API
180
+
181
+ :param silent: silent mode (no signals emit)
182
+ :return: result
183
+ """
184
+ try:
185
+ if self.store_id is None:
186
+ self.log("Importing all files...")
187
+ self.window.core.remote_store.openai.files.truncate_local() # clear local DB (all)
188
+ num = self.window.core.api.openai.store.import_stores_files(self.callback) # import all files
189
+ else:
190
+ self.log("Importing files for store: {}".format(self.store_id))
191
+ self.window.core.remote_store.openai.files.truncate_local(self.store_id) # clear local DB (all)
192
+ items = self.window.core.api.openai.store.import_store_files(
193
+ self.store_id,
194
+ [],
195
+ callback=self.callback,
196
+ ) # import store files
197
+ num = len(items)
198
+ if not silent:
199
+ self.signals.finished.emit("import_files", self.store_id, num)
200
+ return True
201
+ except Exception as e:
202
+ self.log("API error: {}".format(e))
203
+ self.signals.error.emit("import_files", e)
204
+ return False
205
+
206
+ def callback(self, msg: str):
207
+ """
208
+ Log callback
209
+
210
+ :param msg: message
211
+ """
212
+ self.log(msg)
213
+
214
+ def log(self, msg: str):
215
+ """
216
+ Log message
217
+
218
+ :param msg: message
219
+ """
220
+ self.signals.log.emit(self.mode, msg)
221
+
222
+ def cleanup(self):
223
+ """Cleanup resources after worker execution."""
224
+ sig = self.signals
225
+ self.signals = None
226
+ if sig is not None:
227
+ try:
228
+ sig.deleteLater()
229
+ except RuntimeError:
230
+ pass
@@ -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: 2026.01.04 19:00:00 #
9
+ # Updated Date: 2026.01.06 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Optional, Dict, Any
@@ -35,6 +35,8 @@ from .audio import Audio
35
35
  from .image import Image
36
36
  from .remote_tools import Remote
37
37
  from .responses import Responses
38
+ from .store import Store
39
+ from .realtime import Realtime
38
40
 
39
41
 
40
42
  class ApiXAI:
@@ -52,6 +54,8 @@ class ApiXAI:
52
54
  self.image = Image(window)
53
55
  self.remote = Remote(window)
54
56
  self.responses = Responses(window)
57
+ self.store = Store(window)
58
+ self.realtime = Realtime(window)
55
59
  self.client: Optional[xai_sdk.Client] = None
56
60
  self.locked = False
57
61
  self.last_client_args: Optional[Dict[str, Any]] = None
@@ -59,7 +63,8 @@ class ApiXAI:
59
63
  def get_client(
60
64
  self,
61
65
  mode: str = MODE_CHAT,
62
- model: ModelItem = None
66
+ model: ModelItem = None,
67
+ management_api_key = None
63
68
  ) -> xai_sdk.Client:
64
69
  """
65
70
  Get or create xAI client.
@@ -69,11 +74,9 @@ class ApiXAI:
69
74
 
70
75
  :param mode: One of MODE_*
71
76
  :param model: ModelItem (optional, not used currently)
77
+ :param management_api_key: Override API key (for management calls)
72
78
  :return: xai_sdk.Client
73
79
  """
74
- if self.client is not None:
75
- return self.client
76
-
77
80
  cfg = self.window.core.config
78
81
  api_key = cfg.get("api_key_xai") or os.environ.get("XAI_API_KEY") or ""
79
82
  timeout = cfg.get("api_native_xai.timeout") # optional
@@ -90,7 +93,13 @@ class ApiXAI:
90
93
  if proxy:
91
94
  kwargs["channel_options"] = []
92
95
  kwargs["channel_options"].append(("grpc.http_proxy", proxy))
96
+ if management_api_key:
97
+ kwargs["management_api_key"] = management_api_key
98
+
99
+ if self.client is not None and self.last_client_args == kwargs:
100
+ return self.client
93
101
 
102
+ self.last_client_args = kwargs
94
103
  self.client = xai_sdk.Client(**kwargs)
95
104
  return self.client
96
105
 
@@ -129,8 +138,20 @@ class ApiXAI:
129
138
  MODE_COMPLETION,
130
139
  MODE_CHAT,
131
140
  MODE_AUDIO,
132
- MODE_RESEARCH
141
+ MODE_RESEARCH,
142
+ MODE_AUDIO
133
143
  ):
144
+ if mode == MODE_AUDIO and stream:
145
+ # Realtime API for audio streaming
146
+ is_realtime = self.realtime.begin(
147
+ context=context,
148
+ model=model,
149
+ extra=extra or {},
150
+ rt_signals=rt_signals
151
+ )
152
+ if is_realtime:
153
+ return True
154
+
134
155
  # Audio TTS is not exposed via public SDK; treat MODE_AUDIO as chat input.
135
156
  # NOTE: for grok-3 use Chat completions, for > grok-4 use Chat responses
136
157
  if use_responses_api: