pygpt-net 2.7.3__py3-none-any.whl → 2.7.5__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 (157) hide show
  1. pygpt_net/CHANGELOG.txt +15 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +382 -350
  4. pygpt_net/app_core.py +4 -2
  5. pygpt_net/controller/__init__.py +5 -1
  6. pygpt_net/controller/assistant/assistant.py +1 -4
  7. pygpt_net/controller/assistant/batch.py +5 -504
  8. pygpt_net/controller/assistant/editor.py +5 -5
  9. pygpt_net/controller/assistant/files.py +16 -16
  10. pygpt_net/controller/chat/attachment.py +5 -1
  11. pygpt_net/controller/chat/handler/google_stream.py +307 -1
  12. pygpt_net/controller/chat/handler/worker.py +8 -1
  13. pygpt_net/controller/chat/image.py +15 -3
  14. pygpt_net/controller/dialogs/confirm.py +73 -101
  15. pygpt_net/controller/files/files.py +3 -1
  16. pygpt_net/controller/lang/mapping.py +9 -9
  17. pygpt_net/controller/layout/layout.py +2 -2
  18. pygpt_net/controller/painter/capture.py +50 -1
  19. pygpt_net/controller/presets/presets.py +2 -1
  20. pygpt_net/controller/remote_store/__init__.py +12 -0
  21. pygpt_net/{provider/core/assistant_file/db_sqlite → controller/remote_store/google}/__init__.py +2 -2
  22. pygpt_net/controller/remote_store/google/batch.py +402 -0
  23. pygpt_net/controller/remote_store/google/store.py +615 -0
  24. pygpt_net/controller/remote_store/openai/__init__.py +12 -0
  25. pygpt_net/controller/remote_store/openai/batch.py +524 -0
  26. pygpt_net/controller/{assistant → remote_store/openai}/store.py +63 -60
  27. pygpt_net/controller/remote_store/remote_store.py +35 -0
  28. pygpt_net/controller/theme/nodes.py +2 -1
  29. pygpt_net/controller/ui/mode.py +5 -1
  30. pygpt_net/controller/ui/ui.py +36 -2
  31. pygpt_net/core/assistants/assistants.py +3 -15
  32. pygpt_net/core/db/database.py +5 -3
  33. pygpt_net/core/filesystem/url.py +4 -1
  34. pygpt_net/core/locale/placeholder.py +35 -0
  35. pygpt_net/core/remote_store/__init__.py +12 -0
  36. pygpt_net/core/remote_store/google/__init__.py +11 -0
  37. pygpt_net/core/remote_store/google/files.py +224 -0
  38. pygpt_net/core/remote_store/google/store.py +248 -0
  39. pygpt_net/core/remote_store/openai/__init__.py +11 -0
  40. pygpt_net/core/{assistants → remote_store/openai}/files.py +26 -19
  41. pygpt_net/core/{assistants → remote_store/openai}/store.py +32 -15
  42. pygpt_net/core/remote_store/remote_store.py +24 -0
  43. pygpt_net/core/render/web/helpers.py +5 -0
  44. pygpt_net/data/config/config.json +8 -5
  45. pygpt_net/data/config/models.json +77 -3
  46. pygpt_net/data/config/settings.json +45 -14
  47. pygpt_net/data/css/web-blocks.css +3 -0
  48. pygpt_net/data/css/web-chatgpt.css +3 -0
  49. pygpt_net/data/locale/locale.de.ini +43 -41
  50. pygpt_net/data/locale/locale.en.ini +56 -44
  51. pygpt_net/data/locale/locale.es.ini +43 -41
  52. pygpt_net/data/locale/locale.fr.ini +43 -41
  53. pygpt_net/data/locale/locale.it.ini +43 -41
  54. pygpt_net/data/locale/locale.pl.ini +43 -41
  55. pygpt_net/data/locale/locale.uk.ini +43 -41
  56. pygpt_net/data/locale/locale.zh.ini +43 -41
  57. pygpt_net/data/locale/plugin.cmd_history.de.ini +1 -1
  58. pygpt_net/data/locale/plugin.cmd_history.en.ini +1 -1
  59. pygpt_net/data/locale/plugin.cmd_history.es.ini +1 -1
  60. pygpt_net/data/locale/plugin.cmd_history.fr.ini +1 -1
  61. pygpt_net/data/locale/plugin.cmd_history.it.ini +1 -1
  62. pygpt_net/data/locale/plugin.cmd_history.pl.ini +1 -1
  63. pygpt_net/data/locale/plugin.cmd_history.uk.ini +1 -1
  64. pygpt_net/data/locale/plugin.cmd_history.zh.ini +1 -1
  65. pygpt_net/data/locale/plugin.cmd_mouse_control.en.ini +14 -0
  66. pygpt_net/data/locale/plugin.cmd_web.de.ini +1 -1
  67. pygpt_net/data/locale/plugin.cmd_web.en.ini +1 -1
  68. pygpt_net/data/locale/plugin.cmd_web.es.ini +1 -1
  69. pygpt_net/data/locale/plugin.cmd_web.fr.ini +1 -1
  70. pygpt_net/data/locale/plugin.cmd_web.it.ini +1 -1
  71. pygpt_net/data/locale/plugin.cmd_web.pl.ini +1 -1
  72. pygpt_net/data/locale/plugin.cmd_web.uk.ini +1 -1
  73. pygpt_net/data/locale/plugin.cmd_web.zh.ini +1 -1
  74. pygpt_net/data/locale/plugin.idx_llama_index.de.ini +2 -2
  75. pygpt_net/data/locale/plugin.idx_llama_index.en.ini +2 -2
  76. pygpt_net/data/locale/plugin.idx_llama_index.es.ini +2 -2
  77. pygpt_net/data/locale/plugin.idx_llama_index.fr.ini +2 -2
  78. pygpt_net/data/locale/plugin.idx_llama_index.it.ini +2 -2
  79. pygpt_net/data/locale/plugin.idx_llama_index.pl.ini +2 -2
  80. pygpt_net/data/locale/plugin.idx_llama_index.uk.ini +2 -2
  81. pygpt_net/data/locale/plugin.idx_llama_index.zh.ini +2 -2
  82. pygpt_net/item/assistant.py +1 -211
  83. pygpt_net/item/ctx.py +3 -1
  84. pygpt_net/item/store.py +238 -0
  85. pygpt_net/launcher.py +115 -55
  86. pygpt_net/migrations/Version20260102190000.py +35 -0
  87. pygpt_net/migrations/__init__.py +3 -1
  88. pygpt_net/plugin/cmd_mouse_control/config.py +470 -1
  89. pygpt_net/plugin/cmd_mouse_control/plugin.py +488 -22
  90. pygpt_net/plugin/cmd_mouse_control/worker.py +464 -87
  91. pygpt_net/plugin/cmd_mouse_control/worker_sandbox.py +729 -0
  92. pygpt_net/plugin/idx_llama_index/config.py +2 -2
  93. pygpt_net/preload.py +243 -0
  94. pygpt_net/provider/api/google/__init__.py +16 -54
  95. pygpt_net/provider/api/google/chat.py +546 -129
  96. pygpt_net/provider/api/google/computer.py +190 -0
  97. pygpt_net/provider/api/google/image.py +74 -6
  98. pygpt_net/provider/api/google/realtime/realtime.py +2 -2
  99. pygpt_net/provider/api/google/remote_tools.py +93 -0
  100. pygpt_net/provider/api/google/store.py +546 -0
  101. pygpt_net/provider/api/google/video.py +9 -4
  102. pygpt_net/provider/api/google/worker/__init__.py +0 -0
  103. pygpt_net/provider/api/google/worker/importer.py +392 -0
  104. pygpt_net/provider/api/openai/computer.py +10 -1
  105. pygpt_net/provider/api/openai/image.py +42 -19
  106. pygpt_net/provider/api/openai/store.py +6 -6
  107. pygpt_net/provider/api/openai/video.py +27 -2
  108. pygpt_net/provider/api/openai/worker/importer.py +24 -24
  109. pygpt_net/provider/api/x_ai/image.py +25 -2
  110. pygpt_net/provider/core/config/patch.py +23 -1
  111. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +3 -3
  112. pygpt_net/provider/core/model/patch.py +17 -3
  113. pygpt_net/provider/core/preset/json_file.py +13 -7
  114. pygpt_net/provider/core/{assistant_file → remote_file}/__init__.py +1 -1
  115. pygpt_net/provider/core/{assistant_file → remote_file}/base.py +9 -9
  116. pygpt_net/provider/core/remote_file/db_sqlite/__init__.py +12 -0
  117. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/patch.py +1 -1
  118. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/provider.py +23 -20
  119. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/storage.py +35 -27
  120. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/utils.py +5 -4
  121. pygpt_net/provider/core/{assistant_store → remote_store}/__init__.py +1 -1
  122. pygpt_net/provider/core/{assistant_store → remote_store}/base.py +10 -10
  123. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/__init__.py +1 -1
  124. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/patch.py +1 -1
  125. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/provider.py +16 -15
  126. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/storage.py +30 -23
  127. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/utils.py +5 -4
  128. pygpt_net/provider/core/{assistant_store → remote_store}/json_file.py +9 -9
  129. pygpt_net/provider/llms/google.py +2 -2
  130. pygpt_net/ui/base/config_dialog.py +3 -2
  131. pygpt_net/ui/dialog/assistant.py +3 -3
  132. pygpt_net/ui/dialog/plugins.py +3 -1
  133. pygpt_net/ui/dialog/remote_store_google.py +539 -0
  134. pygpt_net/ui/dialog/{assistant_store.py → remote_store_openai.py} +95 -95
  135. pygpt_net/ui/dialogs.py +5 -3
  136. pygpt_net/ui/layout/chat/attachments_uploaded.py +3 -3
  137. pygpt_net/ui/layout/chat/input.py +20 -2
  138. pygpt_net/ui/layout/chat/painter.py +6 -4
  139. pygpt_net/ui/layout/toolbox/computer_env.py +26 -8
  140. pygpt_net/ui/layout/toolbox/image.py +5 -5
  141. pygpt_net/ui/layout/toolbox/video.py +5 -4
  142. pygpt_net/ui/main.py +84 -3
  143. pygpt_net/ui/menu/tools.py +13 -5
  144. pygpt_net/ui/widget/dialog/base.py +3 -10
  145. pygpt_net/ui/widget/dialog/remote_store_google.py +56 -0
  146. pygpt_net/ui/widget/dialog/{assistant_store.py → remote_store_openai.py} +9 -9
  147. pygpt_net/ui/widget/element/button.py +4 -4
  148. pygpt_net/ui/widget/lists/remote_store_google.py +248 -0
  149. pygpt_net/ui/widget/lists/{assistant_store.py → remote_store_openai.py} +21 -21
  150. pygpt_net/ui/widget/option/checkbox_list.py +47 -9
  151. pygpt_net/ui/widget/option/combo.py +158 -4
  152. pygpt_net/ui/widget/textarea/input_extra.py +664 -0
  153. {pygpt_net-2.7.3.dist-info → pygpt_net-2.7.5.dist-info}/METADATA +48 -9
  154. {pygpt_net-2.7.3.dist-info → pygpt_net-2.7.5.dist-info}/RECORD +157 -130
  155. {pygpt_net-2.7.3.dist-info → pygpt_net-2.7.5.dist-info}/LICENSE +0 -0
  156. {pygpt_net-2.7.3.dist-info → pygpt_net-2.7.5.dist-info}/WHEEL +0 -0
  157. {pygpt_net-2.7.3.dist-info → pygpt_net-2.7.5.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,392 @@
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.02 20: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 (Google File Search)
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
+ if mode == "import_files":
37
+ self.window.controller.remote_store.google.batch.handle_imported_files_failed(err)
38
+ elif mode == "truncate_files":
39
+ self.window.controller.remote_store.google.batch.handle_truncated_files_failed(err)
40
+ elif mode == "upload_files":
41
+ self.window.controller.remote_store.google.batch.handle_uploaded_files_failed(err)
42
+ elif mode in "vector_stores":
43
+ self.window.controller.remote_store.google.batch.handle_imported_stores_failed(err)
44
+ elif mode in "truncate_vector_stores":
45
+ self.window.controller.remote_store.google.batch.handle_truncated_stores_failed(err)
46
+ elif mode in "refresh_vector_stores":
47
+ self.window.controller.remote_store.google.batch.handle_refreshed_stores_failed(err)
48
+
49
+ @Slot(str, str, int)
50
+ def handle_finished(self, mode: str, store_id: str = None, num: int = 0):
51
+ """
52
+ Handle thread finished signal
53
+
54
+ :param mode: mode
55
+ :param store_id: store ID
56
+ :param num: number of affected items
57
+ """
58
+ if mode == "import_files":
59
+ self.window.controller.remote_store.google.batch.handle_imported_files(num)
60
+ elif mode == "truncate_files":
61
+ self.window.controller.remote_store.google.batch.handle_truncated_files(store_id, num)
62
+ elif mode == "upload_files":
63
+ self.window.controller.remote_store.google.batch.handle_uploaded_files(num)
64
+ elif mode == "vector_stores":
65
+ self.window.controller.remote_store.google.batch.handle_imported_stores(num)
66
+ elif mode == "truncate_vector_stores":
67
+ self.window.controller.remote_store.google.batch.handle_truncated_stores(num)
68
+ elif mode == "refresh_vector_stores":
69
+ self.window.controller.remote_store.google.batch.handle_refreshed_stores(num)
70
+
71
+ @Slot(str, str)
72
+ def handle_status(self, mode: str, msg: str):
73
+ """
74
+ Handle thread status change signal
75
+
76
+ :param mode: mode
77
+ :param msg: message
78
+ """
79
+ self.window.controller.assistant.batch.handle_status_change(mode, msg)
80
+
81
+ @Slot(str, str)
82
+ def handle_log(self, mode: str, msg: str):
83
+ """
84
+ Handle thread log message signal
85
+
86
+ :param mode: mode
87
+ :param msg: message
88
+ """
89
+ self.window.controller.assistant.threads.log(mode + ": " + msg)
90
+
91
+ def import_assistants(self):
92
+ """Import assistants (kept for parity; no-op for Google if unused)"""
93
+ self.worker = ImportWorker()
94
+ self.worker.window = self.window
95
+ self.worker.mode = "assistants"
96
+ self.connect_signals(self.worker)
97
+ self.window.threadpool.start(self.worker)
98
+
99
+ def import_vector_stores(self):
100
+ """Import File Search stores"""
101
+ self.worker = ImportWorker()
102
+ self.worker.window = self.window
103
+ self.worker.mode = "vector_stores"
104
+ self.connect_signals(self.worker)
105
+ self.window.threadpool.start(self.worker)
106
+
107
+ def truncate_vector_stores(self):
108
+ """Truncate File Search stores"""
109
+ self.worker = ImportWorker()
110
+ self.worker.window = self.window
111
+ self.worker.mode = "truncate_vector_stores"
112
+ self.connect_signals(self.worker)
113
+ self.window.threadpool.start(self.worker)
114
+
115
+ def truncate_files(self, store_id: str = None):
116
+ """
117
+ Truncate documents
118
+
119
+ :param store_id: store name ('fileSearchStores/...').
120
+ """
121
+ self.worker = ImportWorker()
122
+ self.worker.window = self.window
123
+ self.worker.mode = "truncate_files"
124
+ self.worker.store_id = store_id
125
+ self.connect_signals(self.worker)
126
+ self.window.threadpool.start(self.worker)
127
+
128
+ def upload_files(self, store_id: str, files: list = None):
129
+ """
130
+ Upload files
131
+
132
+ :param store_id: store name ('fileSearchStores/...').
133
+ :param files: list of file paths
134
+ """
135
+ print("Uploading files: {}".format(files))
136
+ print("Store ID: {}".format(store_id))
137
+ self.worker = ImportWorker()
138
+ self.worker.window = self.window
139
+ self.worker.mode = "upload_files"
140
+ self.worker.store_id = store_id
141
+ self.worker.files = files or []
142
+ self.connect_signals(self.worker)
143
+ self.window.threadpool.start(self.worker)
144
+
145
+ def refresh_vector_stores(self):
146
+ """Refresh File Search stores"""
147
+ self.worker = ImportWorker()
148
+ self.worker.window = self.window
149
+ self.worker.mode = "refresh_vector_stores"
150
+ self.connect_signals(self.worker)
151
+ self.window.threadpool.start(self.worker)
152
+
153
+ def import_files(self, store_id: str = None):
154
+ """
155
+ Import File Search documents
156
+
157
+ :param store_id: store name ('fileSearchStores/...').
158
+ """
159
+ self.worker = ImportWorker()
160
+ self.worker.window = self.window
161
+ self.worker.mode = "import_files"
162
+ self.worker.store_id = store_id
163
+ self.connect_signals(self.worker)
164
+ self.window.threadpool.start(self.worker)
165
+
166
+ def connect_signals(self, worker):
167
+ """
168
+ Connect signals
169
+
170
+ :param worker: worker instance
171
+ """
172
+ worker.signals.finished.connect(self.handle_finished)
173
+ worker.signals.error.connect(self.handle_error)
174
+ worker.signals.status.connect(self.handle_status)
175
+ worker.signals.log.connect(self.handle_log)
176
+
177
+
178
+ class ImportWorkerSignals(QObject):
179
+ """Import worker signals"""
180
+ status = Signal(str, str) # mode, message
181
+ finished = Signal(str, str, int) # mode, store_id, num
182
+ error = Signal(str, object) # mode, error
183
+ log = Signal(str, str) # mode, message
184
+
185
+
186
+ class ImportWorker(QRunnable):
187
+ """Import worker (Google)"""
188
+ def __init__(self, *args, **kwargs):
189
+ super().__init__()
190
+ self.signals = ImportWorkerSignals()
191
+ self.window = None
192
+ self.mode = "assistants"
193
+ self.assistant = None
194
+ self.store_id = None
195
+ self.files = []
196
+
197
+ @Slot()
198
+ def run(self):
199
+ """Importer thread"""
200
+ try:
201
+ if self.mode == "vector_stores":
202
+ if self.import_vector_stores():
203
+ self.import_files()
204
+ elif self.mode == "truncate_vector_stores":
205
+ self.truncate_vector_stores()
206
+ elif self.mode == "refresh_vector_stores":
207
+ self.refresh_vector_stores()
208
+ elif self.mode == "truncate_files":
209
+ self.truncate_files()
210
+ elif self.mode == "import_files":
211
+ self.import_files()
212
+ elif self.mode == "upload_files":
213
+ self.upload_files()
214
+ except Exception as e:
215
+ self.signals.error.emit(self.mode, e)
216
+ finally:
217
+ self.cleanup()
218
+
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
+ def import_vector_stores(self, silent: bool = False) -> bool:
234
+ """
235
+ Import File Search stores
236
+
237
+ :param silent: silent mode (no signals emit)
238
+ """
239
+ try:
240
+ self.log("Importing File Search stores...")
241
+ self.window.core.remote_store.google.clear()
242
+ items = {}
243
+ self.window.core.api.google.store.import_stores(items, callback=self.callback)
244
+ self.window.core.remote_store.google.import_items(items)
245
+ if not silent:
246
+ self.signals.finished.emit("vector_stores", self.store_id, len(items))
247
+ return True
248
+ except Exception as e:
249
+ self.log("API error: {}".format(e))
250
+ self.signals.error.emit("vector_stores", e)
251
+ return False
252
+
253
+ def truncate_vector_stores(self, silent: bool = False) -> bool:
254
+ """
255
+ Truncate all File Search stores in API
256
+
257
+ :param silent: silent mode
258
+ """
259
+ try:
260
+ self.log("Truncating stores...")
261
+ num = self.window.core.api.google.store.remove_all(callback=self.callback)
262
+ self.window.core.remote_store.google.items = {}
263
+ self.window.core.remote_store.google.save()
264
+ if not silent:
265
+ self.signals.finished.emit("truncate_vector_stores", self.store_id, num)
266
+ return True
267
+ except Exception as e:
268
+ self.log("API error: {}".format(e))
269
+ self.signals.error.emit("truncate_vector_stores", e)
270
+ return False
271
+
272
+ def refresh_vector_stores(self, silent: bool = False) -> bool:
273
+ """
274
+ Refresh all File Search stores in API
275
+
276
+ :param silent: silent mode
277
+ """
278
+ try:
279
+ self.log("Refreshing stores...")
280
+ num = 0
281
+ stores = self.window.core.remote_store.google.items
282
+ for id in stores:
283
+ store = stores[id]
284
+ try:
285
+ self.window.controller.remote_store.google.refresh_store(store, update=False)
286
+ num += 1
287
+ except Exception as e:
288
+ self.log("Failed to refresh store: {}".format(id))
289
+ self.window.core.debug.log(e)
290
+ if not silent:
291
+ self.signals.finished.emit("refresh_vector_stores", self.store_id, num)
292
+ return True
293
+ except Exception as e:
294
+ self.log("API error: {}".format(e))
295
+ self.signals.error.emit("refresh_vector_stores", e)
296
+ return False
297
+
298
+ def truncate_files(self, silent: bool = False) -> bool:
299
+ """
300
+ Truncate documents in API
301
+
302
+ :param silent: silent mode
303
+ """
304
+ try:
305
+ if self.store_id is None:
306
+ self.log("Truncating all documents in all stores...")
307
+ self.window.core.remote_store.google.files.truncate() # clear all locally and remote
308
+ num = self.window.core.api.google.store.remove_from_stores()
309
+ else:
310
+ self.log("Truncating documents for store: {}".format(self.store_id))
311
+ self.window.core.remote_store.google.files.truncate(self.store_id)
312
+ num = self.window.core.api.google.store.remove_from_store(self.store_id)
313
+ if not silent:
314
+ self.signals.finished.emit("truncate_files", self.store_id, num)
315
+ return True
316
+ except Exception as e:
317
+ self.log("API error: {}".format(e))
318
+ self.signals.error.emit("truncate_files", e)
319
+ return False
320
+
321
+ def upload_files(self, silent: bool = False) -> bool:
322
+ """
323
+ Upload files directly to a File Search store (creates Documents)
324
+
325
+ :param silent: silent mode
326
+ """
327
+ num = 0
328
+ try:
329
+ self.log("Uploading files to File Search store...")
330
+ for file in self.files:
331
+ try:
332
+ doc = self.window.core.api.google.store.upload_to_store(self.store_id, file)
333
+ if doc is not None:
334
+ self.window.core.remote_store.google.files.insert(self.store_id, doc)
335
+ msg = "Uploaded file: {}/{}".format((num + 1), len(self.files))
336
+ self.signals.status.emit("upload_files", msg)
337
+ self.log(msg)
338
+ num += 1
339
+ else:
340
+ self.signals.status.emit("upload_files", "Failed to upload file: {}".format(os.path.basename(file)))
341
+ except Exception as e:
342
+ self.window.core.debug.log(e)
343
+ self.signals.status.emit("upload_files", "Failed to upload file: {}".format(os.path.basename(file)))
344
+ if not silent:
345
+ self.signals.finished.emit("upload_files", self.store_id, num)
346
+ return True
347
+ except Exception as e:
348
+ self.log("API error: {}".format(e))
349
+ self.signals.error.emit("upload_files", e)
350
+ return False
351
+
352
+ def import_files(self, silent: bool = False) -> bool:
353
+ """
354
+ Import documents from API
355
+
356
+ :param silent: silent mode
357
+ """
358
+ try:
359
+ if self.store_id is None:
360
+ self.log("Importing all documents...")
361
+ self.window.core.remote_store.google.files.truncate_local() # clear local DB (all)
362
+ num = self.window.core.api.google.store.import_stores_files(self.callback) # import all
363
+ else:
364
+ self.log("Importing documents for store: {}".format(self.store_id))
365
+ self.window.core.remote_store.google.files.truncate_local(self.store_id)
366
+ items = self.window.core.api.google.store.import_store_files(self.store_id, [], callback=self.callback)
367
+ num = len(items)
368
+ if not silent:
369
+ self.signals.finished.emit("import_files", self.store_id, num)
370
+ return True
371
+ except Exception as e:
372
+ self.log("API error: {}".format(e))
373
+ self.signals.error.emit("import_files", e)
374
+ return False
375
+
376
+ def callback(self, msg: str):
377
+ """Log callback"""
378
+ self.log(msg)
379
+
380
+ def log(self, msg: str):
381
+ """Log message"""
382
+ self.signals.log.emit(self.mode, msg)
383
+
384
+ def cleanup(self):
385
+ """Cleanup resources after worker execution."""
386
+ sig = self.signals
387
+ self.signals = None
388
+ if sig is not None:
389
+ try:
390
+ sig.deleteLater()
391
+ except RuntimeError:
392
+ 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: 2025.07.26 18:00:00 #
9
+ # Updated Date: 2026.01.02 02:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import json
@@ -39,11 +39,20 @@ class Computer:
39
39
  Get Computer use tool
40
40
  :return: dict
41
41
  """
42
+ is_sandbox = bool(self.window.core.config.get("remote_tools.computer_use.sandbox", False))
42
43
  env = self.get_current_env()
43
44
  screen = self.window.app.primaryScreen()
44
45
  size = screen.size()
45
46
  screen_x = size.width()
46
47
  screen_y = size.height()
48
+
49
+ # if sandbox, get resolution from plugin settings (Playwright viewport)
50
+ if is_sandbox:
51
+ try:
52
+ screen_x = int(self.window.core.plugins.get_option("cmd_mouse_control", "sandbox_viewport_w"))
53
+ screen_y = int(self.window.core.plugins.get_option("cmd_mouse_control", "sandbox_viewport_h"))
54
+ except Exception:
55
+ pass
47
56
  return {
48
57
  "type": "computer_use_preview",
49
58
  "display_width": screen_x,
@@ -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.12.30 22:00:00 #
9
+ # Updated Date: 2025.12.31 16:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import base64
@@ -59,6 +59,7 @@ class Image:
59
59
  inline = extra.get("inline", False)
60
60
  sub_mode = self.MODE_GENERATE
61
61
  image_id = extra.get("image_id") # previous image reference for remix
62
+ extra_prompt = extra.get("extra_prompt", "")
62
63
 
63
64
  # if attachments then switch mode to EDIT
64
65
  attachments = context.attachments
@@ -74,28 +75,29 @@ class Image:
74
75
  prompt_model = self.window.core.models.get(tmp_model)
75
76
 
76
77
  # worker
77
- self.worker = ImageWorker()
78
- self.worker.window = self.window
79
- self.worker.client = self.window.core.api.openai.get_client()
80
- self.worker.ctx = ctx
81
- self.worker.mode = sub_mode # mode can be "generate" or "edit"
82
- self.worker.attachments = attachments # attachments for edit mode
83
- self.worker.raw = self.window.core.config.get('img_raw')
84
- self.worker.model = model.id # model ID for generate image, e.g. "dall-e-3"
85
- self.worker.model_prompt = prompt_model # model for generate prompt, not image!
86
- self.worker.input_prompt = prompt
87
- self.worker.system_prompt = self.window.core.prompt.get('img')
88
- self.worker.num = num
89
- self.worker.inline = inline
90
- self.worker.image_id = image_id # remix: previous image path/identifier
78
+ worker = ImageWorker()
79
+ worker.window = self.window
80
+ worker.client = self.window.core.api.openai.get_client()
81
+ worker.ctx = ctx
82
+ worker.mode = sub_mode # mode can be "generate" or "edit"
83
+ worker.attachments = attachments # attachments for edit mode
84
+ worker.raw = self.window.core.config.get('img_raw')
85
+ worker.model = model.id # model ID for generate image, e.g. "dall-e-3"
86
+ worker.model_prompt = prompt_model # model for generate prompt, not image!
87
+ worker.input_prompt = prompt
88
+ worker.system_prompt = self.window.core.prompt.get('img')
89
+ worker.num = num
90
+ worker.inline = inline
91
+ worker.extra_prompt = extra_prompt
92
+ worker.image_id = image_id # remix: previous image path/identifier
91
93
 
92
94
  # config
93
95
  if self.window.core.config.has('img_quality'):
94
- self.worker.quality = self.window.core.config.get('img_quality')
96
+ worker.quality = self.window.core.config.get('img_quality')
95
97
  if self.window.core.config.has('img_resolution'):
96
- self.worker.resolution = self.window.core.config.get('img_resolution')
98
+ worker.resolution = self.window.core.config.get('img_resolution')
97
99
 
98
- # signals
100
+ self.worker = worker
99
101
  self.worker.signals.finished.connect(self.window.core.image.handle_finished)
100
102
  self.worker.signals.finished_inline.connect(self.window.core.image.handle_finished_inline)
101
103
  self.worker.signals.status.connect(self.window.core.image.handle_status)
@@ -145,6 +147,7 @@ class ImageWorker(QRunnable):
145
147
  self.input_prompt: Optional[str] = None
146
148
  self.system_prompt = None
147
149
  self.inline = False
150
+ self.extra_prompt: Optional[str] = None
148
151
  self.num = 1
149
152
  self.image_id: Optional[str] = None # previous image reference for remix
150
153
 
@@ -245,6 +248,13 @@ class ImageWorker(QRunnable):
245
248
  self.signals.error.emit(e)
246
249
  self.signals.status.emit(trans('img.status.prompt.error') + ": " + str(e))
247
250
 
251
+ # Fallback negative prompt injection (OpenAI Images API has no native negative_prompt field)
252
+ if self.extra_prompt and str(self.extra_prompt).strip():
253
+ try:
254
+ self.input_prompt = self._merge_negative_prompt(self.input_prompt or "", self.extra_prompt)
255
+ except Exception:
256
+ pass
257
+
248
258
  self.signals.status.emit(trans('img.status.generating') + ": {}...".format(self.input_prompt))
249
259
 
250
260
  paths: List[str] = [] # downloaded images paths
@@ -407,4 +417,17 @@ class ImageWorker(QRunnable):
407
417
  try:
408
418
  sig.deleteLater()
409
419
  except RuntimeError:
410
- pass
420
+ pass
421
+
422
+ # ---------- prompt utilities ----------
423
+
424
+ @staticmethod
425
+ def _merge_negative_prompt(prompt: str, negative: Optional[str]) -> str:
426
+ """
427
+ Append a negative prompt to the main text prompt for providers without a native negative_prompt field.
428
+ """
429
+ base = (prompt or "").strip()
430
+ neg = (negative or "").strip()
431
+ if not neg:
432
+ return base
433
+ return (base + ("\n" if base else "") + f"Negative prompt: {neg}").strip()
@@ -6,13 +6,13 @@
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.02 22:00:00 #
9
+ # Updated Date: 2026.01.02 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
13
13
  from typing import Optional, List
14
14
 
15
- from pygpt_net.item.assistant import AssistantStoreItem
15
+ from pygpt_net.item.store import RemoteStoreItem
16
16
 
17
17
 
18
18
  class Store:
@@ -273,7 +273,7 @@ class Store:
273
273
  for remote in stores.data:
274
274
  id = remote.id
275
275
  if id not in items:
276
- items[id] = AssistantStoreItem()
276
+ items[id] = RemoteStoreItem()
277
277
  tmp_name = remote.name
278
278
  if tmp_name is None:
279
279
  items[id].is_thread = True # tmp store for thread
@@ -281,8 +281,8 @@ class Store:
281
281
  items[id].id = id
282
282
  items[id].name = tmp_name
283
283
  items[id].file_ids = []
284
- items[id].status = self.window.core.assistants.store.parse_status(remote)
285
- self.window.core.assistants.store.append_status(items[id], items[id].status)
284
+ items[id].status = self.window.core.remote_store.openai.parse_status(remote)
285
+ self.window.core.remote_store.openai.append_status(items[id], items[id].status)
286
286
  self.log("Imported vector store: " + id, callback)
287
287
  # next page
288
288
  if stores.has_more:
@@ -633,7 +633,7 @@ class Store:
633
633
  if id not in items:
634
634
  items.append(id)
635
635
  data = self.get_file(remote.id)
636
- self.window.core.assistants.files.insert(store_id, data) # add remote file to DB
636
+ self.window.core.remote_store.openai.files.insert(store_id, data) # add remote file to DB
637
637
  msg = "Imported file ID {} to store {}".format(remote.id, store_id)
638
638
  self.log(msg, callback)
639
639
  except Exception as e:
@@ -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.12.30 22:00:00 #
9
+ # Updated Date: 2025.12.31 16:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import datetime
@@ -60,6 +60,7 @@ class Video:
60
60
  num = int(extra.get("num", 1))
61
61
  inline = bool(extra.get("inline", False))
62
62
  video_id = extra.get("video_id")
63
+ extra_prompt = extra.get("extra_prompt", "")
63
64
 
64
65
  # decide sub-mode based on attachments (image-to-video when image is attached)
65
66
  sub_mode = self.MODE_GENERATE
@@ -86,6 +87,7 @@ class Video:
86
87
  worker.raw = self.window.core.config.get('img_raw')
87
88
  worker.num = num
88
89
  worker.inline = inline
90
+ worker.extra_prompt = extra_prompt
89
91
  worker.video_id = video_id
90
92
 
91
93
  # optional params (app-level options)
@@ -157,6 +159,7 @@ class VideoWorker(QRunnable):
157
159
  self.input_prompt = ""
158
160
  self.system_prompt = ""
159
161
  self.inline = False
162
+ self.extra_prompt: Optional[str] = None
160
163
  self.video_id = None
161
164
  self.raw = False
162
165
  self.num = 1
@@ -193,6 +196,14 @@ class VideoWorker(QRunnable):
193
196
  self.signals.error.emit(e)
194
197
  self.signals.status.emit(trans('vid.status.prompt.error') + ": " + str(e))
195
198
 
199
+ # Negative prompt fallback: inject constraints into the text prompt (Sora has no native negative_prompt field)
200
+ if self.extra_prompt and str(self.extra_prompt).strip():
201
+ try:
202
+ self.input_prompt = self._merge_negative_prompt(self.input_prompt, self.extra_prompt)
203
+ except Exception:
204
+ # do not fail generation if merge fails
205
+ pass
206
+
196
207
  # Sora API accepts a single video per create call; honor app's num but cap to 1 per job
197
208
  _ = max(1, min(self.num, self.max_per_job))
198
209
 
@@ -609,4 +620,18 @@ class VideoWorker(QRunnable):
609
620
  except Exception:
610
621
  pass
611
622
 
612
- return " ".join(parts).strip()
623
+ return " ".join(parts).strip()
624
+
625
+ # ---------- prompt utilities ----------
626
+
627
+ @staticmethod
628
+ def _merge_negative_prompt(prompt: str, negative: Optional[str]) -> str:
629
+ """
630
+ Append a negative prompt to the main text prompt for providers without a native negative_prompt field.
631
+ """
632
+ base = (prompt or "").strip()
633
+ neg = (negative or "").strip()
634
+ if not neg:
635
+ return base
636
+ # Keep the user's original prompt intact and add clear constraint instructions.
637
+ return (base + ("\n" if base else "") + f"Negative prompt: {neg}").strip()