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
@@ -0,0 +1,307 @@
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 20:00:00 #
10
+ # ================================================== #
11
+
12
+ import os
13
+ import mimetypes
14
+ import time
15
+ from typing import Optional, List, Dict, Any
16
+
17
+ from .worker.importer import Importer
18
+
19
+
20
+ class Store:
21
+ """
22
+ Anthropic Files API wrapper.
23
+ """
24
+
25
+ # Files API is in beta and requires this header, per official docs
26
+ # https://docs.anthropic.com/en/docs/build-with-claude/files
27
+ BETA_HEADER = ["files-api-2025-04-14"]
28
+
29
+ def __init__(self, window=None):
30
+ """
31
+ Anthropic Files API store wrapper
32
+
33
+ :param window: Window instance
34
+ """
35
+ self.window = window
36
+ self.importer = Importer(window)
37
+
38
+ def get_client(self):
39
+ """
40
+ Get Anthropic client
41
+
42
+ :return: anthropic.Anthropic
43
+ """
44
+ return self.window.core.api.anthropic.get_client()
45
+
46
+ def log(self, msg: str, callback: Optional[callable] = None):
47
+ """
48
+ Log message
49
+
50
+ :param msg: message to log
51
+ :param callback: callback log function
52
+ """
53
+ if callback is not None:
54
+ callback(msg)
55
+ else:
56
+ print(msg)
57
+
58
+ def _download_dir(self) -> str:
59
+ """
60
+ Resolve target download directory (uses download.dir if set).
61
+ """
62
+ if self.window.core.config.has("download.dir") and self.window.core.config.get("download.dir") != "":
63
+ dir_path = os.path.join(
64
+ self.window.core.config.get_user_dir('data'),
65
+ self.window.core.config.get("download.dir"),
66
+ )
67
+ else:
68
+ dir_path = self.window.core.config.get_user_dir('data')
69
+ os.makedirs(dir_path, exist_ok=True)
70
+ return dir_path
71
+
72
+ def _ensure_unique_path(self, dir_path: str, filename: str) -> str:
73
+ """
74
+ Ensure unique filename in dir, add timestamp prefix if exists.
75
+ """
76
+ path = os.path.join(dir_path, filename)
77
+ if os.path.exists(path):
78
+ prefix = time.strftime("%Y%m%d_%H%M%S_")
79
+ path = os.path.join(dir_path, f"{prefix}{filename}")
80
+ return path
81
+
82
+ # -----------------------------
83
+ # Files service
84
+ # -----------------------------
85
+
86
+ def get_file(self, file_id: str):
87
+ """
88
+ Retrieve file metadata by ID using the SDK method names present in the installed version.
89
+ Prefer 'retrieve', fallback to 'get' only if available to avoid attribute errors.
90
+ """
91
+ client = self.get_client()
92
+ files_api = client.beta.files
93
+ if hasattr(files_api, "retrieve"):
94
+ return files_api.retrieve(file_id, betas=self.BETA_HEADER)
95
+ if hasattr(files_api, "get"):
96
+ return files_api.get(file_id, betas=self.BETA_HEADER)
97
+ raise AttributeError("Anthropic Files API client does not expose 'retrieve' or 'get' for file metadata.")
98
+
99
+ def upload(self, path: str):
100
+ """
101
+ Upload file to Anthropic Files API.
102
+
103
+ Per SDK guidance, the Python client supports passing a PathLike, raw bytes,
104
+ or a (filename, contents, media_type) tuple. Using the tuple form ensures the
105
+ correct filename and MIME type are sent in multipart/form-data.
106
+
107
+ :param path: file path
108
+ :return: file object or None
109
+ """
110
+ client = self.get_client()
111
+ if not os.path.exists(path):
112
+ return None
113
+
114
+ filename = os.path.basename(path)
115
+ mime_type = mimetypes.guess_type(path)[0] or "application/octet-stream"
116
+ with open(path, "rb") as f:
117
+ data = f.read()
118
+ file_part = (filename, data, mime_type)
119
+
120
+ files_api = client.beta.files
121
+
122
+ try:
123
+ return files_api.upload(file=file_part, betas=self.BETA_HEADER)
124
+ except Exception:
125
+ if hasattr(files_api, "create"):
126
+ return files_api.create(file=file_part, betas=self.BETA_HEADER)
127
+ raise
128
+
129
+ def delete_file(self, file_id: str) -> Optional[str]:
130
+ """
131
+ Delete a file by ID. Returns the file_id on success.
132
+ """
133
+ client = self.get_client()
134
+ res = client.beta.files.delete(file_id, betas=self.BETA_HEADER)
135
+ if res is not None:
136
+ return file_id
137
+
138
+ def download(self, file_id: str, path: str) -> bool:
139
+ """
140
+ Download a file content to a local path.
141
+
142
+ :param file_id: Anthropic Files API file id
143
+ :param path: target file path
144
+ :return: True on success
145
+ """
146
+ client = self.get_client()
147
+ try:
148
+ # SDK returns raw bytes for file content
149
+ data = client.beta.files.content(file_id, betas=self.BETA_HEADER)
150
+ except Exception:
151
+ data = None
152
+
153
+ if data is None:
154
+ return False
155
+
156
+ try:
157
+ if hasattr(data, "read"):
158
+ content = data.read()
159
+ else:
160
+ content = data if isinstance(data, (bytes, bytearray)) else bytes(data)
161
+ with open(path, "wb") as f:
162
+ f.write(content)
163
+ return True
164
+ except Exception:
165
+ return False
166
+
167
+ def download_to_dir(self, file_id: str, prefer_name: Optional[str] = None) -> Optional[str]:
168
+ """
169
+ Download a file by ID into configured download directory.
170
+
171
+ :param file_id: Anthropic Files API file id
172
+ :param prefer_name: optional filename preference
173
+ :return: saved file path or None
174
+ """
175
+ dir_path = self._download_dir()
176
+ filename = None
177
+
178
+ if prefer_name:
179
+ filename = os.path.basename(prefer_name)
180
+
181
+ if not filename:
182
+ try:
183
+ meta = self.get_file(file_id)
184
+ except Exception:
185
+ meta = None
186
+ if meta is not None:
187
+ for attr in ("filename", "name", "id"):
188
+ try:
189
+ val = getattr(meta, attr, None)
190
+ if isinstance(val, str) and val:
191
+ filename = os.path.basename(val)
192
+ break
193
+ except Exception:
194
+ pass
195
+ if not filename:
196
+ filename = file_id
197
+ else:
198
+ filename = file_id
199
+
200
+ if not os.path.splitext(filename)[1] and meta is not None:
201
+ try:
202
+ mime = getattr(meta, "mime_type", None)
203
+ ext = mimetypes.guess_extension(mime or "") or ""
204
+ if ext and not filename.endswith(ext):
205
+ filename = filename + ext
206
+ except Exception:
207
+ pass
208
+
209
+ path = self._ensure_unique_path(dir_path, filename)
210
+ if self.download(file_id, path):
211
+ return path
212
+ return None
213
+
214
+ def get_files_ids(self, limit: int = 1000) -> List[str]:
215
+ """
216
+ Return a list of file IDs. Falls back to filename only if id is missing.
217
+ """
218
+ client = self.get_client()
219
+ items: List[str] = []
220
+ pager = client.beta.files.list(limit=limit, betas=self.BETA_HEADER)
221
+
222
+ data = getattr(pager, "data", None)
223
+ if data is None:
224
+ data = pager
225
+
226
+ for f in data:
227
+ fid = getattr(f, "id", None) or getattr(f, "filename", None)
228
+ if fid and fid not in items:
229
+ items.append(fid)
230
+ return items
231
+
232
+ def remove_files(self, callback: Optional[callable] = None) -> int:
233
+ """
234
+ Remove all files from remote storage. Returns number of successfully removed files.
235
+ """
236
+ num = 0
237
+ files = self.get_files_ids()
238
+ for file_id in files:
239
+ self.log("Removing file: " + file_id, callback)
240
+ try:
241
+ res = self.delete_file(file_id)
242
+ if res:
243
+ num += 1
244
+ except Exception as e:
245
+ msg = "Error removing file {}: {}".format(file_id, str(e))
246
+ self.log(msg, callback)
247
+ return num
248
+
249
+ def remove_file(self, file_id: str, callback: Optional[callable] = None) -> bool:
250
+ """
251
+ Remove a single file by ID. Raises on errors to allow upstream handling.
252
+ """
253
+ self.log("Removing file: " + file_id, callback)
254
+ try:
255
+ res = self.delete_file(file_id)
256
+ return res is not None
257
+ except Exception as e:
258
+ msg = "Error removing file {}: {}".format(file_id, str(e))
259
+ self.log(msg, callback)
260
+ raise
261
+
262
+ def import_files(self, callback: Optional[callable] = None) -> int:
263
+ """
264
+ Import all files from Anthropic Files API into local DB.
265
+
266
+ :param callback: log callback
267
+ :return: number of imported files
268
+ """
269
+ client = self.get_client()
270
+ total = 0
271
+ pager = client.beta.files.list(limit=1000, betas=self.BETA_HEADER)
272
+
273
+ data = getattr(pager, "data", None)
274
+ if data is None:
275
+ data = pager
276
+
277
+ for f in data:
278
+ try:
279
+ self.window.core.remote_store.anthropic.files.insert("files", f)
280
+ total += 1
281
+ except Exception as e:
282
+ self.log("Error importing file {}: {}".format(getattr(f, "id", "?"), e), callback)
283
+ return total
284
+
285
+ def get_files_stats(self) -> Dict[str, Any]:
286
+ """
287
+ Compute files stats (count + total bytes).
288
+ """
289
+ client = self.get_client()
290
+ count = 0
291
+ total_bytes = 0
292
+ pager = client.beta.files.list(limit=1000, betas=self.BETA_HEADER)
293
+
294
+ data = getattr(pager, "data", None)
295
+ if data is None:
296
+ data = pager
297
+
298
+ for f in data:
299
+ count += 1
300
+ try:
301
+ size = getattr(f, "size_bytes", None)
302
+ if size is None and hasattr(f, "size"):
303
+ size = getattr(f, "size")
304
+ total_bytes += int(size or 0)
305
+ except Exception:
306
+ pass
307
+ return {"count": count, "total_bytes": total_bytes}
@@ -40,6 +40,70 @@ def process_anthropic_chunk(ctx, core, state, chunk) -> Optional[str]:
40
40
  except Exception:
41
41
  pass
42
42
 
43
+ def _to_plain(obj):
44
+ try:
45
+ if hasattr(obj, "model_dump"):
46
+ return obj.model_dump()
47
+ if hasattr(obj, "to_dict"):
48
+ return obj.to_dict()
49
+ except Exception:
50
+ pass
51
+ if isinstance(obj, dict):
52
+ return {k: _to_plain(v) for k, v in obj.items()}
53
+ if isinstance(obj, (list, tuple)):
54
+ return [_to_plain(x) for x in obj]
55
+ return obj
56
+
57
+ def _walk_for_file_ids(o, acc: set):
58
+ if o is None:
59
+ return
60
+ if isinstance(o, dict):
61
+ for k, v in o.items():
62
+ if k == "file_id" and isinstance(v, str) and v.startswith("file_"):
63
+ acc.add(v)
64
+ else:
65
+ _walk_for_file_ids(v, acc)
66
+ elif isinstance(o, (list, tuple)):
67
+ for it in o:
68
+ _walk_for_file_ids(it, acc)
69
+
70
+ def _download_files(ids: set):
71
+ if not ids:
72
+ return
73
+ if not hasattr(state, "anthropic_downloaded_ids"):
74
+ state.anthropic_downloaded_ids = set()
75
+ saved = []
76
+ for fid in ids:
77
+ if fid in state.anthropic_downloaded_ids:
78
+ continue
79
+ try:
80
+ path = core.api.anthropic.store.download_to_dir(fid)
81
+ except Exception:
82
+ path = None
83
+ if path:
84
+ saved.append(path)
85
+ state.anthropic_downloaded_ids.add(fid)
86
+ if saved:
87
+ try:
88
+ loc = core.filesystem.make_local_list(saved)
89
+ except Exception:
90
+ loc = saved
91
+ if not isinstance(ctx.files, list):
92
+ ctx.files = []
93
+ for p in loc:
94
+ if p not in ctx.files:
95
+ ctx.files.append(p)
96
+ imgs = []
97
+ for p in loc:
98
+ ext = p.lower().rsplit(".", 1)[-1] if "." in p else ""
99
+ if ext in ["png", "jpg", "jpeg", "gif", "bmp", "tiff", "webp"]:
100
+ imgs.append(p)
101
+ if imgs:
102
+ if not isinstance(ctx.images, list):
103
+ ctx.images = []
104
+ for p in imgs:
105
+ if p not in ctx.images:
106
+ ctx.images.append(p)
43
107
 
44
108
  # --- Top-level delta objects (when SDK yields deltas directly) ---
45
109
  if etype == "text_delta":
@@ -95,6 +159,17 @@ def process_anthropic_chunk(ctx, core, state, chunk) -> Optional[str]:
95
159
  except Exception:
96
160
  pass
97
161
 
162
+ try:
163
+ cb = getattr(chunk, "content_block", None)
164
+ if cb:
165
+ btype = getattr(cb, "type", "") or ""
166
+ if btype.endswith("_tool_result"):
167
+ ids = set()
168
+ _walk_for_file_ids(_to_plain(getattr(cb, "content", None)), ids)
169
+ _download_files(ids)
170
+ except Exception:
171
+ pass
172
+
98
173
  try:
99
174
  cb = getattr(chunk, "content_block", None)
100
175
  if cb and getattr(cb, "type", "") == "web_search_tool_result":
File without changes
@@ -0,0 +1,278 @@
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 (Anthropic Files)
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
+ batch = self.window.controller.remote_store.batch
31
+ if mode == "import_files":
32
+ batch.handle_imported_files_failed(err)
33
+ elif mode == "truncate_files":
34
+ batch.handle_truncated_files_failed(err)
35
+ elif mode == "upload_files":
36
+ batch.handle_uploaded_files_failed(err)
37
+ elif mode in "vector_stores":
38
+ batch.handle_imported_stores_failed(err)
39
+ elif mode in "truncate_vector_stores":
40
+ batch.handle_truncated_stores_failed(err)
41
+ elif mode in "refresh_vector_stores":
42
+ batch.handle_refreshed_stores_failed(err)
43
+
44
+ @Slot(str, str, int)
45
+ def handle_finished(self, mode: str, store_id: str = None, num: int = 0):
46
+ batch = self.window.controller.remote_store.batch
47
+ if mode == "import_files":
48
+ batch.handle_imported_files(num)
49
+ elif mode == "truncate_files":
50
+ batch.handle_truncated_files(store_id, num)
51
+ elif mode == "upload_files":
52
+ batch.handle_uploaded_files(num)
53
+ elif mode == "vector_stores":
54
+ batch.handle_imported_stores(num)
55
+ elif mode == "truncate_vector_stores":
56
+ batch.handle_truncated_stores(num)
57
+ elif mode == "refresh_vector_stores":
58
+ batch.handle_refreshed_stores(num)
59
+
60
+ @Slot(str, str)
61
+ def handle_status(self, mode: str, msg: str):
62
+ self.window.controller.assistant.batch.handle_status_change(mode, msg)
63
+
64
+ @Slot(str, str)
65
+ def handle_log(self, mode: str, msg: str):
66
+ self.window.controller.assistant.threads.log(mode + ": " + msg)
67
+
68
+ def import_vector_stores(self):
69
+ """Create/ensure pseudo-store and import files list."""
70
+ self.worker = ImportWorker()
71
+ self.worker.window = self.window
72
+ self.worker.mode = "vector_stores"
73
+ self.connect_signals(self.worker)
74
+ self.window.threadpool.start(self.worker)
75
+
76
+ def truncate_vector_stores(self):
77
+ """Clear local pseudo-store metadata (no remote action)."""
78
+ self.worker = ImportWorker()
79
+ self.worker.window = self.window
80
+ self.worker.mode = "truncate_vector_stores"
81
+ self.connect_signals(self.worker)
82
+ self.window.threadpool.start(self.worker)
83
+
84
+ def truncate_files(self, store_id: str = None):
85
+ """Remove all files via Files API."""
86
+ self.worker = ImportWorker()
87
+ self.worker.window = self.window
88
+ self.worker.mode = "truncate_files"
89
+ self.worker.store_id = store_id
90
+ self.connect_signals(self.worker)
91
+ self.window.threadpool.start(self.worker)
92
+
93
+ def upload_files(self, store_id: str, files: list = None):
94
+ """Upload files to Files API."""
95
+ self.worker = ImportWorker()
96
+ self.worker.window = self.window
97
+ self.worker.mode = "upload_files"
98
+ self.worker.store_id = store_id
99
+ self.worker.files = files or []
100
+ self.connect_signals(self.worker)
101
+ self.window.threadpool.start(self.worker)
102
+
103
+ def refresh_vector_stores(self):
104
+ """Refresh pseudo-store status."""
105
+ self.worker = ImportWorker()
106
+ self.worker.window = self.window
107
+ self.worker.mode = "refresh_vector_stores"
108
+ self.connect_signals(self.worker)
109
+ self.window.threadpool.start(self.worker)
110
+
111
+ def import_files(self, store_id: str = None):
112
+ """Import files from Files API."""
113
+ self.worker = ImportWorker()
114
+ self.worker.window = self.window
115
+ self.worker.mode = "import_files"
116
+ self.worker.store_id = store_id
117
+ self.connect_signals(self.worker)
118
+ self.window.threadpool.start(self.worker)
119
+
120
+ def connect_signals(self, worker):
121
+ worker.signals.finished.connect(self.handle_finished)
122
+ worker.signals.error.connect(self.handle_error)
123
+ worker.signals.status.connect(self.handle_status)
124
+ worker.signals.log.connect(self.handle_log)
125
+
126
+
127
+ class ImportWorkerSignals(QObject):
128
+ status = Signal(str, str) # mode, message
129
+ finished = Signal(str, str, int) # mode, store_id, num
130
+ error = Signal(str, object) # mode, error
131
+ log = Signal(str, str) # mode, message
132
+
133
+
134
+ class ImportWorker(QRunnable):
135
+ """Import worker (Anthropic)"""
136
+ def __init__(self, *args, **kwargs):
137
+ super().__init__()
138
+ self.signals = ImportWorkerSignals()
139
+ self.window = None
140
+ self.mode = "vector_stores"
141
+ self.store_id = "files"
142
+ self.files = []
143
+
144
+ @Slot()
145
+ def run(self):
146
+ try:
147
+ if self.mode == "vector_stores":
148
+ if self.import_vector_stores():
149
+ self.import_files()
150
+ elif self.mode == "truncate_vector_stores":
151
+ self.truncate_vector_stores()
152
+ elif self.mode == "refresh_vector_stores":
153
+ self.refresh_vector_stores()
154
+ elif self.mode == "truncate_files":
155
+ self.truncate_files()
156
+ elif self.mode == "import_files":
157
+ self.import_files()
158
+ elif self.mode == "upload_files":
159
+ self.upload_files()
160
+ except Exception as e:
161
+ self.signals.error.emit(self.mode, e)
162
+ finally:
163
+ self.cleanup()
164
+
165
+ def import_vector_stores(self, silent: bool = False) -> bool:
166
+ """
167
+ Ensure pseudo-store exists locally.
168
+ """
169
+ try:
170
+ self.log("Ensuring Anthropic workspace store...")
171
+ items = {}
172
+ # Single pseudo-store object
173
+ store = self.window.core.remote_store.anthropic.create("Files")
174
+ items[store.id] = store
175
+ self.window.core.remote_store.anthropic.import_items(items)
176
+ if not silent:
177
+ self.signals.finished.emit("vector_stores", self.store_id, 1)
178
+ return True
179
+ except Exception as e:
180
+ self.log("API error: {}".format(e))
181
+ self.signals.error.emit("vector_stores", e)
182
+ return False
183
+
184
+ def truncate_vector_stores(self, silent: bool = False) -> bool:
185
+ try:
186
+ self.log("Truncating local pseudo-store...")
187
+ self.window.core.remote_store.anthropic.items = {}
188
+ self.window.core.remote_store.anthropic.save()
189
+ if not silent:
190
+ self.signals.finished.emit("truncate_vector_stores", self.store_id, 1)
191
+ return True
192
+ except Exception as e:
193
+ self.log("API error: {}".format(e))
194
+ self.signals.error.emit("truncate_vector_stores", e)
195
+ return False
196
+
197
+ def refresh_vector_stores(self, silent: bool = False) -> bool:
198
+ try:
199
+ self.log("Refreshing workspace status...")
200
+ # Ensure exists
201
+ if "files" not in self.window.core.remote_store.anthropic.items:
202
+ self.import_vector_stores(silent=True)
203
+ store = self.window.core.remote_store.anthropic.items["files"]
204
+ self.window.controller.remote_store.refresh_store(store, update=False, provider="anthropic")
205
+ if not silent:
206
+ self.signals.finished.emit("refresh_vector_stores", self.store_id, 1)
207
+ return True
208
+ except Exception as e:
209
+ self.log("API error: {}".format(e))
210
+ self.signals.error.emit("refresh_vector_stores", e)
211
+ return False
212
+
213
+ def truncate_files(self, silent: bool = False) -> bool:
214
+ try:
215
+ self.log("Removing all files via Anthropic Files API...")
216
+ num = self.window.core.api.anthropic.store.remove_files(callback=self.callback)
217
+ self.window.core.remote_store.anthropic.files.truncate_local()
218
+ if not silent:
219
+ self.signals.finished.emit("truncate_files", self.store_id, num)
220
+ return True
221
+ except Exception as e:
222
+ self.log("API error: {}".format(e))
223
+ self.signals.error.emit("truncate_files", e)
224
+ return False
225
+
226
+ def upload_files(self, silent: bool = False) -> bool:
227
+ num = 0
228
+ try:
229
+ self.log("Uploading files to Anthropic Files API...")
230
+ for path in self.files:
231
+ try:
232
+ f = self.window.core.api.anthropic.store.upload(path)
233
+ if f is not None:
234
+ self.window.core.remote_store.anthropic.files.insert("files", f)
235
+ num += 1
236
+ msg = "Uploaded file: {}/{}".format(num, len(self.files))
237
+ self.signals.status.emit("upload_files", msg)
238
+ self.log(msg)
239
+ else:
240
+ self.signals.status.emit("upload_files", "Failed to upload: {}".format(os.path.basename(path)))
241
+ except Exception as e:
242
+ self.window.core.debug.log(e)
243
+ self.signals.status.emit("upload_files", "Failed to upload: {}".format(os.path.basename(path)))
244
+ if not silent:
245
+ self.signals.finished.emit("upload_files", self.store_id, num)
246
+ return True
247
+ except Exception as e:
248
+ self.log("API error: {}".format(e))
249
+ self.signals.error.emit("upload_files", e)
250
+ return False
251
+
252
+ def import_files(self, silent: bool = False) -> bool:
253
+ try:
254
+ self.log("Importing files from Anthropic Files API...")
255
+ self.window.core.remote_store.anthropic.files.truncate_local()
256
+ num = self.window.core.api.anthropic.store.import_files(callback=self.callback)
257
+ if not silent:
258
+ self.signals.finished.emit("import_files", self.store_id, num)
259
+ return True
260
+ except Exception as e:
261
+ self.log("API error: {}".format(e))
262
+ self.signals.error.emit("import_files", e)
263
+ return False
264
+
265
+ def callback(self, msg: str):
266
+ self.log(msg)
267
+
268
+ def log(self, msg: str):
269
+ self.signals.log.emit(self.mode, msg)
270
+
271
+ def cleanup(self):
272
+ sig = self.signals
273
+ self.signals = None
274
+ if sig is not None:
275
+ try:
276
+ sig.deleteLater()
277
+ except RuntimeError:
278
+ pass