pygpt-net 2.7.7__py3-none-any.whl → 2.7.8__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 (93) hide show
  1. pygpt_net/CHANGELOG.txt +7 -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/remote_store/{google/batch.py → batch.py} +209 -252
  11. pygpt_net/controller/remote_store/remote_store.py +982 -13
  12. pygpt_net/core/command/command.py +0 -0
  13. pygpt_net/core/db/viewer.py +1 -1
  14. pygpt_net/core/realtime/worker.py +3 -1
  15. pygpt_net/{controller/remote_store/google → core/remote_store/anthropic}/__init__.py +0 -1
  16. pygpt_net/core/remote_store/anthropic/files.py +211 -0
  17. pygpt_net/core/remote_store/anthropic/store.py +208 -0
  18. pygpt_net/core/remote_store/openai/store.py +5 -4
  19. pygpt_net/core/remote_store/remote_store.py +5 -1
  20. pygpt_net/{controller/remote_store/openai → core/remote_store/xai}/__init__.py +0 -1
  21. pygpt_net/core/remote_store/xai/files.py +225 -0
  22. pygpt_net/core/remote_store/xai/store.py +219 -0
  23. pygpt_net/data/config/config.json +9 -6
  24. pygpt_net/data/config/models.json +5 -4
  25. pygpt_net/data/config/settings.json +54 -1
  26. pygpt_net/data/icons/folder_eye.svg +1 -0
  27. pygpt_net/data/icons/folder_eye_filled.svg +1 -0
  28. pygpt_net/data/icons/folder_open.svg +1 -0
  29. pygpt_net/data/icons/folder_open_filled.svg +1 -0
  30. pygpt_net/data/locale/locale.de.ini +4 -3
  31. pygpt_net/data/locale/locale.en.ini +14 -4
  32. pygpt_net/data/locale/locale.es.ini +4 -3
  33. pygpt_net/data/locale/locale.fr.ini +4 -3
  34. pygpt_net/data/locale/locale.it.ini +4 -3
  35. pygpt_net/data/locale/locale.pl.ini +5 -4
  36. pygpt_net/data/locale/locale.uk.ini +4 -3
  37. pygpt_net/data/locale/locale.zh.ini +4 -3
  38. pygpt_net/icons.qrc +4 -0
  39. pygpt_net/icons_rc.py +282 -138
  40. pygpt_net/provider/api/anthropic/__init__.py +2 -0
  41. pygpt_net/provider/api/anthropic/chat.py +84 -1
  42. pygpt_net/provider/api/anthropic/store.py +307 -0
  43. pygpt_net/provider/api/anthropic/stream.py +75 -0
  44. pygpt_net/provider/api/anthropic/worker/__init__.py +0 -0
  45. pygpt_net/provider/api/anthropic/worker/importer.py +278 -0
  46. pygpt_net/provider/api/google/chat.py +59 -2
  47. pygpt_net/provider/api/google/store.py +124 -3
  48. pygpt_net/provider/api/google/stream.py +91 -24
  49. pygpt_net/provider/api/google/worker/importer.py +16 -28
  50. pygpt_net/provider/api/openai/assistants.py +2 -2
  51. pygpt_net/provider/api/openai/store.py +4 -1
  52. pygpt_net/provider/api/openai/worker/importer.py +19 -61
  53. pygpt_net/provider/api/openai/worker/importer_assistants.py +230 -0
  54. pygpt_net/provider/api/x_ai/__init__.py +30 -6
  55. pygpt_net/provider/api/x_ai/audio.py +43 -11
  56. pygpt_net/provider/api/x_ai/chat.py +92 -4
  57. pygpt_net/provider/api/x_ai/realtime/__init__.py +12 -0
  58. pygpt_net/provider/api/x_ai/realtime/client.py +1825 -0
  59. pygpt_net/provider/api/x_ai/realtime/realtime.py +198 -0
  60. pygpt_net/provider/api/x_ai/remote_tools.py +19 -1
  61. pygpt_net/provider/api/x_ai/store.py +610 -0
  62. pygpt_net/provider/api/x_ai/stream.py +30 -9
  63. pygpt_net/provider/api/x_ai/worker/importer.py +308 -0
  64. pygpt_net/provider/audio_input/xai_grok_voice.py +390 -0
  65. pygpt_net/provider/audio_output/xai_tts.py +325 -0
  66. pygpt_net/provider/core/config/patch.py +18 -3
  67. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +2 -2
  68. pygpt_net/provider/core/model/patch.py +13 -0
  69. pygpt_net/tools/image_viewer/tool.py +334 -34
  70. pygpt_net/tools/image_viewer/ui/dialogs.py +317 -21
  71. pygpt_net/ui/dialog/assistant.py +1 -1
  72. pygpt_net/ui/dialog/plugins.py +13 -5
  73. pygpt_net/ui/dialog/remote_store.py +552 -0
  74. pygpt_net/ui/dialogs.py +3 -5
  75. pygpt_net/ui/layout/ctx/ctx_list.py +58 -7
  76. pygpt_net/ui/menu/tools.py +6 -13
  77. pygpt_net/ui/widget/dialog/{remote_store_google.py → remote_store.py} +10 -10
  78. pygpt_net/ui/widget/element/button.py +4 -4
  79. pygpt_net/ui/widget/image/display.py +2 -2
  80. pygpt_net/ui/widget/lists/context.py +2 -2
  81. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.8.dist-info}/METADATA +9 -2
  82. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.8.dist-info}/RECORD +82 -70
  83. pygpt_net/controller/remote_store/google/store.py +0 -615
  84. pygpt_net/controller/remote_store/openai/batch.py +0 -524
  85. pygpt_net/controller/remote_store/openai/store.py +0 -699
  86. pygpt_net/ui/dialog/remote_store_google.py +0 -539
  87. pygpt_net/ui/dialog/remote_store_openai.py +0 -539
  88. pygpt_net/ui/widget/dialog/remote_store_openai.py +0 -56
  89. pygpt_net/ui/widget/lists/remote_store_google.py +0 -248
  90. pygpt_net/ui/widget/lists/remote_store_openai.py +0 -317
  91. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.8.dist-info}/LICENSE +0 -0
  92. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.8.dist-info}/WHEEL +0 -0
  93. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.8.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 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,23 @@ 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:
145
+ raise NotImplementedError("Not available. xAI realtime audio streaming coming soon!")
146
+
147
+ if mode == MODE_AUDIO and stream:
148
+ # Realtime API for audio streaming
149
+ is_realtime = self.realtime.begin(
150
+ context=context,
151
+ model=model,
152
+ extra=extra or {},
153
+ rt_signals=rt_signals
154
+ )
155
+ if is_realtime:
156
+ return True
157
+
134
158
  # Audio TTS is not exposed via public SDK; treat MODE_AUDIO as chat input.
135
159
  # NOTE: for grok-3 use Chat completions, for > grok-4 use Chat responses
136
160
  if use_responses_api:
@@ -6,27 +6,59 @@
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.05 01:00:00 #
9
+ # Updated Date: 2026.01.06 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from typing import Tuple
12
+ import base64
13
+ from typing import Optional, Union, List, Dict, Any
14
+
15
+ from pygpt_net.core.bridge.context import MultimodalContext
13
16
 
14
17
 
15
18
  class Audio:
16
19
  def __init__(self, window=None):
17
20
  """
18
- Audio helpers for xAI.
19
-
20
- Note: As of now, the public xAI Python SDK does not expose TTS/STT or realtime audio APIs.
21
- This class exists to keep provider surface compatible.
21
+ Audio input wrapper
22
22
 
23
23
  :param window: Window instance
24
24
  """
25
25
  self.window = window
26
26
 
27
- # Placeholders to keep interface parity
28
- def build_part(self, multimodal_ctx) -> None:
29
- return None
27
+ def build_content(
28
+ self,
29
+ content: Optional[Union[str, list]] = None,
30
+ multimodal_ctx: Optional[MultimodalContext] = None,
31
+ ) -> List[Dict[str, Any]]:
32
+ """
33
+ Build audio content from multimodal context
34
+
35
+ :param content: previous content or input prompt
36
+ :param multimodal_ctx: multimodal context
37
+ :return: List of contents
38
+ """
39
+ if not isinstance(content, list):
40
+ if content:
41
+ content = [
42
+ {
43
+ "type": "text",
44
+ "text": str(content),
45
+ }
46
+ ]
47
+ else:
48
+ content = [] # if empty input return empty list
49
+
50
+ # abort if no audio input provided
51
+ if not multimodal_ctx.is_audio_input:
52
+ return content
30
53
 
31
- def extract_first_audio_part(self, response) -> Tuple[None, None]:
32
- return None, None
54
+ encoded = base64.b64encode(multimodal_ctx.audio_data).decode('utf-8')
55
+ audio_format = multimodal_ctx.audio_format # wav by default
56
+ audio_data = {
57
+ "type": "input_audio",
58
+ "input_audio": {
59
+ "data": encoded,
60
+ "format": audio_format,
61
+ }
62
+ }
63
+ content.append(audio_data)
64
+ return content
@@ -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.03 17:00:00 #
9
+ # Updated Date: 2026.01.05 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from __future__ import annotations
@@ -34,7 +34,6 @@ class Chat:
34
34
  """
35
35
  self.window = window
36
36
  self.input_tokens = 0
37
- # Image constraints (can be overridden by config keys below)
38
37
  self.allowed_mimes = {"image/jpeg", "image/png"}
39
38
  self.default_image_max_bytes = 10 * 1024 * 1024 # 10 MiB default
40
39
 
@@ -319,6 +318,12 @@ class Chat:
319
318
  except Exception:
320
319
  pass
321
320
 
321
+ try:
322
+ # Attempt to auto-download file parts or references (file id)
323
+ self._maybe_download_files_from_response(response, ctx)
324
+ except Exception:
325
+ pass
326
+
322
327
  # Usage
323
328
  try:
324
329
  if isinstance(response, dict) and response.get("usage"):
@@ -1089,7 +1094,7 @@ class Chat:
1089
1094
 
1090
1095
  def _collect_images_from_message_parts(self, parts: List[dict], ctx: CtxItem):
1091
1096
  """
1092
- Inspect assistant message parts for image_url outputs and store them.
1097
+ Inspect assistant message parts for image_url outputs and URLs.
1093
1098
  For http(s) URLs -> add to ctx.urls; for data URLs -> save to file and add to ctx.images.
1094
1099
  """
1095
1100
  try:
@@ -1098,6 +1103,25 @@ class Chat:
1098
1103
  for p in parts:
1099
1104
  if not isinstance(p, dict):
1100
1105
  continue
1106
+ if p.get("type") == "file":
1107
+ file_id = p.get("id") or p.get("file_id")
1108
+ if isinstance(file_id, str):
1109
+ try:
1110
+ save = self.window.core.api.xai.store.download_to_dir(file_id)
1111
+ if save:
1112
+ if not isinstance(ctx.files, list):
1113
+ ctx.files = []
1114
+ if save not in ctx.files:
1115
+ ctx.files.append(save)
1116
+ ext = os.path.splitext(save)[1].lower().lstrip(".")
1117
+ if ext in ["png", "jpg", "jpeg", "gif", "bmp", "tiff", "webp"]:
1118
+ if not isinstance(ctx.images, list):
1119
+ ctx.images = []
1120
+ if save not in ctx.images:
1121
+ ctx.images.append(save)
1122
+ except Exception:
1123
+ pass
1124
+ continue
1101
1125
  if p.get("type") != "image_url":
1102
1126
  continue
1103
1127
  img = p.get("image_url") or {}
@@ -1135,4 +1159,68 @@ class Chat:
1135
1159
  """
1136
1160
  Return the locally estimated input tokens count.
1137
1161
  """
1138
- return self.input_tokens
1162
+ return self.input_tokens
1163
+
1164
+ def _maybe_download_files_from_response(self, response, ctx: CtxItem) -> None:
1165
+ """
1166
+ Attempt to download any files referenced by id in response payloads (dict/SDK/proto).
1167
+ """
1168
+ def _walk(o, acc: set):
1169
+ if o is None:
1170
+ return
1171
+ if isinstance(o, dict):
1172
+ fid = o.get("file_id") or o.get("id") if o.get("type") == "file" else None
1173
+ if isinstance(fid, str) and fid.startswith("file-"):
1174
+ acc.add(fid)
1175
+ for v in o.values():
1176
+ _walk(v, acc)
1177
+ elif isinstance(o, (list, tuple)):
1178
+ for it in o:
1179
+ _walk(it, acc)
1180
+
1181
+ ids = set()
1182
+ try:
1183
+ if isinstance(response, dict):
1184
+ _walk(response, ids)
1185
+ else:
1186
+ msg = getattr(response, "message", None) or getattr(response, "output_message", None)
1187
+ if msg:
1188
+ _walk(getattr(msg, "content", None), ids)
1189
+ proto = getattr(response, "proto", None)
1190
+ if proto:
1191
+ ch = getattr(proto, "choices", None) or []
1192
+ if ch:
1193
+ m = getattr(ch[0], "message", None)
1194
+ if m:
1195
+ _walk(getattr(m, "content", None), ids)
1196
+ except Exception:
1197
+ pass
1198
+
1199
+ if not ids:
1200
+ return
1201
+ saved = []
1202
+ for fid in ids:
1203
+ try:
1204
+ p = self.window.core.api.xai.store.download_to_dir(fid)
1205
+ if p:
1206
+ saved.append(p)
1207
+ except Exception:
1208
+ continue
1209
+ if saved:
1210
+ saved = self.window.core.filesystem.make_local_list(saved)
1211
+ if not isinstance(ctx.files, list):
1212
+ ctx.files = []
1213
+ for p in saved:
1214
+ if p not in ctx.files:
1215
+ ctx.files.append(p)
1216
+ imgs = []
1217
+ for p in saved:
1218
+ ext = os.path.splitext(p)[1].lower().lstrip(".")
1219
+ if ext in ["png", "jpg", "jpeg", "gif", "bmp", "tiff", "webp"]:
1220
+ imgs.append(p)
1221
+ if imgs:
1222
+ if not isinstance(ctx.images, list):
1223
+ ctx.images = []
1224
+ for p in imgs:
1225
+ if p not in ctx.images:
1226
+ ctx.images.append(p)
@@ -0,0 +1,12 @@
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.08.31 23:00:00 #
10
+ # ================================================== #
11
+
12
+ from .realtime import Realtime