pygpt-net 2.7.6__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 (120) hide show
  1. pygpt_net/CHANGELOG.txt +13 -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/chat/remote_tools.py +3 -9
  9. pygpt_net/controller/chat/stream.py +2 -2
  10. pygpt_net/controller/chat/{handler/worker.py → stream_worker.py} +13 -35
  11. pygpt_net/controller/dialogs/confirm.py +35 -58
  12. pygpt_net/controller/lang/mapping.py +9 -9
  13. pygpt_net/controller/remote_store/{google/batch.py → batch.py} +209 -252
  14. pygpt_net/controller/remote_store/remote_store.py +982 -13
  15. pygpt_net/core/command/command.py +0 -0
  16. pygpt_net/core/db/viewer.py +1 -1
  17. pygpt_net/core/debug/models.py +2 -2
  18. pygpt_net/core/realtime/worker.py +3 -1
  19. pygpt_net/{controller/remote_store/google → core/remote_store/anthropic}/__init__.py +0 -1
  20. pygpt_net/core/remote_store/anthropic/files.py +211 -0
  21. pygpt_net/core/remote_store/anthropic/store.py +208 -0
  22. pygpt_net/core/remote_store/openai/store.py +5 -4
  23. pygpt_net/core/remote_store/remote_store.py +5 -1
  24. pygpt_net/{controller/remote_store/openai → core/remote_store/xai}/__init__.py +0 -1
  25. pygpt_net/core/remote_store/xai/files.py +225 -0
  26. pygpt_net/core/remote_store/xai/store.py +219 -0
  27. pygpt_net/data/config/config.json +18 -5
  28. pygpt_net/data/config/models.json +193 -4
  29. pygpt_net/data/config/settings.json +179 -36
  30. pygpt_net/data/icons/folder_eye.svg +1 -0
  31. pygpt_net/data/icons/folder_eye_filled.svg +1 -0
  32. pygpt_net/data/icons/folder_open.svg +1 -0
  33. pygpt_net/data/icons/folder_open_filled.svg +1 -0
  34. pygpt_net/data/locale/locale.de.ini +6 -3
  35. pygpt_net/data/locale/locale.en.ini +46 -12
  36. pygpt_net/data/locale/locale.es.ini +6 -3
  37. pygpt_net/data/locale/locale.fr.ini +6 -3
  38. pygpt_net/data/locale/locale.it.ini +6 -3
  39. pygpt_net/data/locale/locale.pl.ini +7 -4
  40. pygpt_net/data/locale/locale.uk.ini +6 -3
  41. pygpt_net/data/locale/locale.zh.ini +6 -3
  42. pygpt_net/icons.qrc +4 -0
  43. pygpt_net/icons_rc.py +282 -138
  44. pygpt_net/plugin/cmd_mouse_control/worker.py +2 -1
  45. pygpt_net/plugin/cmd_mouse_control/worker_sandbox.py +2 -1
  46. pygpt_net/provider/api/anthropic/__init__.py +10 -3
  47. pygpt_net/provider/api/anthropic/chat.py +342 -11
  48. pygpt_net/provider/api/anthropic/computer.py +844 -0
  49. pygpt_net/provider/api/anthropic/remote_tools.py +172 -0
  50. pygpt_net/provider/api/anthropic/store.py +307 -0
  51. pygpt_net/{controller/chat/handler/anthropic_stream.py → provider/api/anthropic/stream.py} +99 -10
  52. pygpt_net/provider/api/anthropic/tools.py +32 -77
  53. pygpt_net/provider/api/anthropic/utils.py +30 -0
  54. pygpt_net/{controller/chat/handler → provider/api/anthropic/worker}/__init__.py +0 -0
  55. pygpt_net/provider/api/anthropic/worker/importer.py +278 -0
  56. pygpt_net/provider/api/google/chat.py +62 -9
  57. pygpt_net/provider/api/google/store.py +124 -3
  58. pygpt_net/{controller/chat/handler/google_stream.py → provider/api/google/stream.py} +92 -25
  59. pygpt_net/provider/api/google/utils.py +185 -0
  60. pygpt_net/provider/api/google/worker/importer.py +16 -28
  61. pygpt_net/provider/api/langchain/__init__.py +0 -0
  62. pygpt_net/{controller/chat/handler/langchain_stream.py → provider/api/langchain/stream.py} +1 -1
  63. pygpt_net/provider/api/llama_index/__init__.py +0 -0
  64. pygpt_net/{controller/chat/handler/llamaindex_stream.py → provider/api/llama_index/stream.py} +1 -1
  65. pygpt_net/provider/api/openai/assistants.py +2 -2
  66. pygpt_net/provider/api/openai/image.py +2 -2
  67. pygpt_net/provider/api/openai/store.py +4 -1
  68. pygpt_net/{controller/chat/handler/openai_stream.py → provider/api/openai/stream.py} +1 -1
  69. pygpt_net/provider/api/openai/utils.py +69 -3
  70. pygpt_net/provider/api/openai/worker/importer.py +19 -61
  71. pygpt_net/provider/api/openai/worker/importer_assistants.py +230 -0
  72. pygpt_net/provider/api/x_ai/__init__.py +138 -15
  73. pygpt_net/provider/api/x_ai/audio.py +43 -11
  74. pygpt_net/provider/api/x_ai/chat.py +92 -4
  75. pygpt_net/provider/api/x_ai/image.py +149 -47
  76. pygpt_net/provider/api/x_ai/realtime/__init__.py +12 -0
  77. pygpt_net/provider/api/x_ai/realtime/client.py +1825 -0
  78. pygpt_net/provider/api/x_ai/realtime/realtime.py +198 -0
  79. pygpt_net/provider/api/x_ai/{remote.py → remote_tools.py} +183 -70
  80. pygpt_net/provider/api/x_ai/responses.py +507 -0
  81. pygpt_net/provider/api/x_ai/store.py +610 -0
  82. pygpt_net/{controller/chat/handler/xai_stream.py → provider/api/x_ai/stream.py} +42 -10
  83. pygpt_net/provider/api/x_ai/tools.py +59 -8
  84. pygpt_net/{controller/chat/handler → provider/api/x_ai}/utils.py +1 -2
  85. pygpt_net/provider/api/x_ai/vision.py +1 -4
  86. pygpt_net/provider/api/x_ai/worker/importer.py +308 -0
  87. pygpt_net/provider/audio_input/xai_grok_voice.py +390 -0
  88. pygpt_net/provider/audio_output/xai_tts.py +325 -0
  89. pygpt_net/provider/core/config/patch.py +39 -3
  90. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +2 -2
  91. pygpt_net/provider/core/model/patch.py +39 -1
  92. pygpt_net/tools/image_viewer/tool.py +334 -34
  93. pygpt_net/tools/image_viewer/ui/dialogs.py +319 -22
  94. pygpt_net/tools/text_editor/ui/dialogs.py +3 -2
  95. pygpt_net/tools/text_editor/ui/widgets.py +0 -0
  96. pygpt_net/ui/dialog/assistant.py +1 -1
  97. pygpt_net/ui/dialog/plugins.py +13 -5
  98. pygpt_net/ui/dialog/remote_store.py +552 -0
  99. pygpt_net/ui/dialogs.py +3 -5
  100. pygpt_net/ui/layout/ctx/ctx_list.py +58 -7
  101. pygpt_net/ui/menu/tools.py +6 -13
  102. pygpt_net/ui/widget/dialog/base.py +16 -5
  103. pygpt_net/ui/widget/dialog/{remote_store_google.py → remote_store.py} +10 -10
  104. pygpt_net/ui/widget/element/button.py +4 -4
  105. pygpt_net/ui/widget/image/display.py +2 -2
  106. pygpt_net/ui/widget/lists/context.py +2 -2
  107. pygpt_net/ui/widget/textarea/editor.py +0 -0
  108. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.dist-info}/METADATA +15 -2
  109. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.dist-info}/RECORD +107 -89
  110. pygpt_net/controller/remote_store/google/store.py +0 -615
  111. pygpt_net/controller/remote_store/openai/batch.py +0 -524
  112. pygpt_net/controller/remote_store/openai/store.py +0 -699
  113. pygpt_net/ui/dialog/remote_store_google.py +0 -539
  114. pygpt_net/ui/dialog/remote_store_openai.py +0 -539
  115. pygpt_net/ui/widget/dialog/remote_store_openai.py +0 -56
  116. pygpt_net/ui/widget/lists/remote_store_google.py +0 -248
  117. pygpt_net/ui/widget/lists/remote_store_openai.py +0 -317
  118. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.dist-info}/LICENSE +0 -0
  119. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.dist-info}/WHEEL +0 -0
  120. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,610 @@
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.06 06:00:00 #
10
+ # ================================================== #
11
+
12
+ import os
13
+ import time
14
+ import mimetypes
15
+ from typing import Optional, List, Dict, Any
16
+
17
+ from pygpt_net.item.store import RemoteStoreItem
18
+ from .worker.importer import Importer
19
+
20
+
21
+ class Store:
22
+ """
23
+ xAI API wrapper.
24
+
25
+ Notes:
26
+ - Existing Files API methods are kept intact for backward compatibility.
27
+ - New Collections API helpers are added with suffix "_collections".
28
+ The xAI Importer and Core use only the Collections methods.
29
+ """
30
+
31
+ def __init__(self, window=None):
32
+ """
33
+ xAI API store wrapper
34
+
35
+ :param window: Window instance
36
+ """
37
+ self.window = window
38
+ self.importer = Importer(window)
39
+
40
+ # -----------------------------
41
+ # Common helpers
42
+ # -----------------------------
43
+
44
+ def get_client(self):
45
+ """
46
+ Get xAI client (xai_sdk.Client or OpenAI-compatible client).
47
+ Requires management_api_key.
48
+ """
49
+ management_api_key = self.window.core.config.get("api_key_management_xai")
50
+ if management_api_key:
51
+ return self.window.core.api.xai.get_client(management_api_key=management_api_key)
52
+ return self.window.core.api.xai.get_client()
53
+
54
+ def log(self, msg: str, callback: Optional[callable] = None):
55
+ if callback is not None:
56
+ callback(msg)
57
+ else:
58
+ print(msg)
59
+
60
+ def _download_dir(self) -> str:
61
+ """
62
+ Resolve target download directory (uses download.dir if set).
63
+ """
64
+ if self.window.core.config.has("download.dir") and self.window.core.config.get("download.dir") != "":
65
+ dir_path = os.path.join(
66
+ self.window.core.config.get_user_dir('data'),
67
+ self.window.core.config.get("download.dir"),
68
+ )
69
+ else:
70
+ dir_path = self.window.core.config.get_user_dir('data')
71
+ os.makedirs(dir_path, exist_ok=True)
72
+ return dir_path
73
+
74
+ def _ensure_unique_path(self, dir_path: str, filename: str) -> str:
75
+ """
76
+ Ensure unique filename in dir, add timestamp prefix if exists.
77
+ """
78
+ path = os.path.join(dir_path, filename)
79
+ if os.path.exists(path):
80
+ prefix = time.strftime("%Y%m%d_%H%M%S_")
81
+ path = os.path.join(dir_path, f"{prefix}{filename}")
82
+ return path
83
+
84
+ def _extract_list(self, obj: Any, prefer: Optional[List[str]] = None) -> List[Any]:
85
+ """
86
+ Extract list-like data from xAI SDK responses.
87
+
88
+ Strategy:
89
+ - Check well-known attributes in preferred order (e.g. 'collections', 'documents', 'data', 'items', 'results')
90
+ - Accept dicts with those keys
91
+ - Accept already-iterables (list/tuple)
92
+ - Fallback to empty list
93
+ """
94
+ if obj is None:
95
+ return []
96
+
97
+ if isinstance(obj, (list, tuple)):
98
+ return list(obj)
99
+
100
+ attrs = prefer or []
101
+ # Append common candidates, keeping stable priority
102
+ for a in ['collections', 'documents', 'data', 'items', 'results']:
103
+ if a not in attrs:
104
+ attrs.append(a)
105
+
106
+ for a in attrs:
107
+ try:
108
+ val = getattr(obj, a, None)
109
+ except Exception:
110
+ val = None
111
+ if val is not None:
112
+ if isinstance(val, (list, tuple)):
113
+ return list(val)
114
+ try:
115
+ return list(val) # handle iterables
116
+ except TypeError:
117
+ pass
118
+
119
+ if isinstance(obj, dict):
120
+ for a in attrs:
121
+ if a in obj and isinstance(obj[a], (list, tuple)):
122
+ return list(obj[a])
123
+ # As last resort, try values
124
+ try:
125
+ return list(obj.values())
126
+ except Exception:
127
+ pass
128
+
129
+ try:
130
+ return list(obj)
131
+ except TypeError:
132
+ return []
133
+
134
+ # -----------------------------
135
+ # Files service (kept)
136
+ # -----------------------------
137
+
138
+ def get_file(self, file_id: str):
139
+ client = self.get_client()
140
+ # Try SDK variants
141
+ if hasattr(client, "files"):
142
+ try:
143
+ return client.files.get(file_id)
144
+ except Exception:
145
+ try:
146
+ return client.files.retrieve(file_id)
147
+ except Exception:
148
+ return client.files.info(file_id)
149
+
150
+ def upload(self, path: str):
151
+ """
152
+ Upload file to xAI Files API.
153
+
154
+ :param path: file path
155
+ :return: file object or None
156
+ """
157
+ client = self.get_client()
158
+ if not os.path.exists(path):
159
+ return None
160
+
161
+ # Prefer SDK upload helper supporting file path / bytes / file object
162
+ try:
163
+ return client.files.upload(path)
164
+ except Exception:
165
+ pass
166
+
167
+ # Fallbacks
168
+ with open(path, "rb") as f:
169
+ data = f.read()
170
+ try:
171
+ return client.files.upload(data, filename=os.path.basename(path))
172
+ except Exception:
173
+ try:
174
+ return client.files.upload(open(path, "rb"), filename=os.path.basename(path))
175
+ except Exception:
176
+ return None
177
+
178
+ def delete_file(self, file_id: str) -> Optional[str]:
179
+ client = self.get_client()
180
+ res = None
181
+ try:
182
+ res = client.files.delete(file_id)
183
+ except Exception:
184
+ try:
185
+ res = client.files.delete(id=file_id)
186
+ except Exception:
187
+ pass
188
+ if res is not None:
189
+ try:
190
+ return getattr(res, "id", file_id)
191
+ except Exception:
192
+ return file_id
193
+
194
+ def download(self, file_id: str, path: str) -> bool:
195
+ """
196
+ Download a file contents to a local path.
197
+
198
+ :param file_id: xAI file id
199
+ :param path: target path
200
+ :return: True on success
201
+ """
202
+ client = self.get_client()
203
+ data = None
204
+ try:
205
+ data = client.files.content(file_id)
206
+ except Exception:
207
+ data = None
208
+
209
+ if data is None:
210
+ try:
211
+ content = client.files.content(file_id)
212
+ data = content if isinstance(content, (bytes, bytearray)) else None
213
+ except Exception:
214
+ data = None
215
+
216
+ if data is None:
217
+ return False
218
+
219
+ try:
220
+ with open(path, "wb") as f:
221
+ f.write(data if isinstance(data, (bytes, bytearray)) else bytes(data))
222
+ return True
223
+ except Exception:
224
+ return False
225
+
226
+ def download_to_dir(self, file_id: str, prefer_name: Optional[str] = None) -> Optional[str]:
227
+ """
228
+ Download a file by ID into configured download directory.
229
+
230
+ :param file_id: xAI file id
231
+ :param prefer_name: optional preferred filename
232
+ :return: saved path or None
233
+ """
234
+ dir_path = self._download_dir()
235
+ filename = None
236
+
237
+ if prefer_name:
238
+ filename = os.path.basename(prefer_name)
239
+
240
+ meta = None
241
+ if not filename:
242
+ try:
243
+ meta = self.get_file(file_id)
244
+ except Exception:
245
+ meta = None
246
+
247
+ if meta is not None and not filename:
248
+ for attr in ("filename", "name", "id"):
249
+ try:
250
+ val = getattr(meta, attr, None)
251
+ if isinstance(val, str) and val:
252
+ filename = os.path.basename(val)
253
+ break
254
+ except Exception:
255
+ pass
256
+
257
+ if not filename:
258
+ filename = file_id
259
+
260
+ if not os.path.splitext(filename)[1] and meta is not None:
261
+ try:
262
+ mime = getattr(meta, "mime_type", None) or getattr(meta, "type", None)
263
+ ext = mimetypes.guess_extension(mime or "") or ""
264
+ if ext and not filename.endswith(ext):
265
+ filename = filename + ext
266
+ except Exception:
267
+ pass
268
+
269
+ path = self._ensure_unique_path(dir_path, filename)
270
+ if self.download(file_id, path):
271
+ return path
272
+ return None
273
+
274
+ def get_files_ids(self, limit: int = 1000) -> List[str]:
275
+ client = self.get_client()
276
+ items = []
277
+ try:
278
+ pager = client.files.list(limit=limit)
279
+ except Exception:
280
+ pager = client.files.list()
281
+ data = self._extract_list(pager, prefer=['data'])
282
+ for f in data:
283
+ fid = getattr(f, "id", None) or getattr(f, "name", None)
284
+ if fid and fid not in items:
285
+ items.append(fid)
286
+ return items
287
+
288
+ def remove_files(self, callback: Optional[callable] = None) -> int:
289
+ num = 0
290
+ files = self.get_files_ids()
291
+ for file_id in files:
292
+ self.log("Removing file: " + file_id, callback)
293
+ try:
294
+ res = self.delete_file(file_id)
295
+ if res:
296
+ num += 1
297
+ except Exception as e:
298
+ msg = "Error removing file {}: {}".format(file_id, str(e))
299
+ self.log(msg, callback)
300
+ return num
301
+
302
+ def remove_file(self, file_id: str, callback: Optional[callable] = None) -> bool:
303
+ self.log("Removing file: " + file_id, callback)
304
+ try:
305
+ res = self.delete_file(file_id)
306
+ return res is not None
307
+ except Exception as e:
308
+ msg = "Error removing file {}: {}".format(file_id, str(e))
309
+ self.log(msg, callback)
310
+ raise
311
+
312
+ def import_files(self, callback: Optional[callable] = None) -> int:
313
+ """
314
+ Import all Files-API files into local DB (workspace-level).
315
+ """
316
+ client = self.get_client()
317
+ total = 0
318
+ try:
319
+ pager = client.files.list(limit=1000)
320
+ except Exception:
321
+ pager = client.files.list()
322
+ data = self._extract_list(pager, prefer=['data'])
323
+ for f in data:
324
+ try:
325
+ self.window.core.remote_store.xai.files.insert("files", f)
326
+ total += 1
327
+ except Exception as e:
328
+ self.log("Error importing file {}: {}".format(getattr(f, "id", "?"), e), callback)
329
+ return total
330
+
331
+ def get_files_stats(self) -> Dict[str, Any]:
332
+ client = self.get_client()
333
+ count = 0
334
+ total_bytes = 0
335
+ try:
336
+ pager = client.files.list(limit=1000)
337
+ except Exception:
338
+ pager = client.files.list()
339
+ data = self._extract_list(pager, prefer=['data'])
340
+ for f in data:
341
+ count += 1
342
+ try:
343
+ size = getattr(f, "size_bytes", None)
344
+ if size is None and hasattr(f, "size"):
345
+ size = getattr(f, "size")
346
+ total_bytes += int(size or 0)
347
+ except Exception:
348
+ pass
349
+ return {"count": count, "total_bytes": total_bytes}
350
+
351
+ # -----------------------------
352
+ # Collections service (new)
353
+ # -----------------------------
354
+
355
+ # Collections: management
356
+
357
+ def create_collection_collections(self, name: str):
358
+ client = self.get_client()
359
+ return client.collections.create(name=name)
360
+
361
+ def update_collection_collections(self, id: str, name: Optional[str] = None):
362
+ client = self.get_client()
363
+ return client.collections.update(id, name=name) if name is not None else client.collections.get(id)
364
+
365
+ def get_collection_collections(self, id: str):
366
+ client = self.get_client()
367
+ return client.collections.get(id)
368
+
369
+ def remove_collection_collections(self, id: str):
370
+ client = self.get_client()
371
+ return client.collections.delete(collection_id=id)
372
+
373
+ def get_collections_ids_collections(self, items: list, limit: int = 100, after: Optional[str] = None) -> list:
374
+ client = self.get_client()
375
+ cols = client.collections.list()
376
+ data = self._extract_list(cols, prefer=['collections', 'data'])
377
+ for c in data:
378
+ cid = getattr(c, "collection_id", None) or (c.get("collection_id") if isinstance(c, dict) else None)
379
+ if cid and cid not in items:
380
+ items.append(cid)
381
+ return items
382
+
383
+ def import_collections_collections(
384
+ self,
385
+ items: Dict[str, RemoteStoreItem],
386
+ callback: Optional[callable] = None
387
+ ) -> Dict[str, RemoteStoreItem]:
388
+ """
389
+ Import Collections into RemoteStoreItem map.
390
+ """
391
+ client = self.get_client()
392
+ cols = client.collections.list()
393
+ data = self._extract_list(cols, prefer=['collections', 'data'])
394
+ for c in data:
395
+ cid = getattr(c, "collection_id", None) or (c.get("collection_id") if isinstance(c, dict) else None)
396
+ if not cid:
397
+ continue
398
+ if cid not in items:
399
+ items[cid] = RemoteStoreItem()
400
+ name = getattr(c, "collection_name", None)
401
+ if name is None and isinstance(c, dict):
402
+ name = c.get("collection_name")
403
+ name = name or ""
404
+ items[cid].id = cid
405
+ items[cid].name = name
406
+ items[cid].file_ids = []
407
+ items[cid].status = {
408
+ "status": "ready",
409
+ "remote_display_name": name,
410
+ }
411
+ items[cid].provider = "xai"
412
+ status, _ = self.window.core.remote_store.xai.get_status_data(cid)
413
+ self.window.core.remote_store.xai.append_status(items[cid], status)
414
+ self.log("Imported collection: " + cid, callback)
415
+ return items
416
+
417
+ def remove_all_collections_collections(self, callback: Optional[callable] = None) -> int:
418
+ """
419
+ Delete all collections.
420
+ """
421
+ num = 0
422
+ ids = self.get_collections_ids_collections([])
423
+ for cid in ids:
424
+ self.log("Removing collection: " + cid, callback)
425
+ try:
426
+ self.remove_collection_collections(cid)
427
+ num += 1
428
+ except Exception as e:
429
+ self.log("Error removing collection {}: {}".format(cid, e), callback)
430
+ return num
431
+
432
+ # Collections: documents membership
433
+
434
+ def add_file_to_collection_collections(self, collection_id: str, file_id: str):
435
+ client = self.get_client()
436
+ try:
437
+ return client.collections.add_document(collection_id=collection_id, file_id=file_id)
438
+ except Exception:
439
+ return client.collections.add_existing_document(collection_id=collection_id, file_id=file_id)
440
+
441
+ def delete_collection_file_collections(self, collection_id: str, file_id: str):
442
+ client = self.get_client()
443
+ return client.collections.remove_document(collection_id=collection_id, file_id=file_id)
444
+
445
+ def remove_store_file(self, store_id: str, file_id: str) -> bool:
446
+ """
447
+ Unified hook used by UI: remove a file from a specific 'store' (collection in xAI).
448
+ """
449
+ try:
450
+ res = self.delete_collection_file_collections(store_id, file_id)
451
+ return res is not None
452
+ except Exception:
453
+ return False
454
+
455
+ def list_collection_documents_raw_(self, collection_id: str, limit: int = 1000) -> List[Any]:
456
+ """
457
+ Internal: list documents of a collection with various SDK method shapes.
458
+ """
459
+ client = self.get_client()
460
+
461
+ # Variant 1: simple list method
462
+ try:
463
+ docs = client.collections.list_documents(collection_id=collection_id, limit=limit)
464
+ return self._extract_list(docs, prefer=['documents', 'data'])
465
+ except Exception:
466
+ pass
467
+
468
+ # Variant 2: nested resource .documents.list(...)
469
+ try:
470
+ docs = client.collections.documents.list(collection_id=collection_id, limit=limit)
471
+ return self._extract_list(docs, prefer=['documents', 'data'])
472
+ except Exception:
473
+ pass
474
+
475
+ # Variant 3: object + list()
476
+ try:
477
+ docs_obj = client.collections.documents(collection_id)
478
+ docs = docs_obj.list(limit=limit)
479
+ return self._extract_list(docs, prefer=['documents', 'data'])
480
+ except Exception:
481
+ pass
482
+
483
+ return []
484
+
485
+ def get_collection_files_ids_collections(self, collection_id: str, items: list, limit: int = 1000) -> list:
486
+ """
487
+ Return document file IDs for a collection.
488
+ """
489
+ data = self.list_collection_documents_raw_(collection_id, limit=limit)
490
+ for d in data:
491
+ fid = d.file_metadata.file_id
492
+ if fid and fid not in items:
493
+ items.append(fid)
494
+ return items
495
+
496
+ def remove_from_collection_collections(self, collection_id: str, callback: Optional[callable] = None) -> int:
497
+ """
498
+ Remove all documents from a specific collection.
499
+ """
500
+ num = 0
501
+ files = self.get_collection_files_ids_collections(collection_id, [])
502
+ for file_id in files:
503
+ self.log("Removing document from collection [{}]: {}".format(collection_id, file_id), callback)
504
+ try:
505
+ self.delete_collection_file_collections(collection_id, file_id)
506
+ num += 1
507
+ except Exception as e:
508
+ self.log("Error removing document {} from collection {}: {}".format(file_id, collection_id, e), callback)
509
+ return num
510
+
511
+ def remove_from_collections_collections(self, callback: Optional[callable] = None) -> int:
512
+ """
513
+ Remove all documents from all collections.
514
+ """
515
+ num = 0
516
+ col_ids = self.get_collections_ids_collections([])
517
+ for cid in col_ids:
518
+ num += self.remove_from_collection_collections(cid, callback=callback)
519
+ return num
520
+
521
+ # Collections: import / upload
522
+
523
+ def import_collection_files_collections(
524
+ self,
525
+ collection_id: str,
526
+ items: list,
527
+ limit: int = 1000,
528
+ callback: Optional[callable] = None
529
+ ) -> list:
530
+ """
531
+ Import and get all collection document IDs.
532
+ """
533
+ docs = self.list_collection_documents_raw_(collection_id, limit=limit)
534
+ for d in docs:
535
+ try:
536
+ fid = d.file_metadata.file_id
537
+ if not fid:
538
+ continue
539
+ if fid not in items:
540
+ items.append(fid)
541
+ data = self.get_file(fid)
542
+ self.window.core.remote_store.xai.files.insert(collection_id, data)
543
+ self.log("Imported file ID {} to collection {}".format(fid, collection_id), callback)
544
+ except Exception as e:
545
+ self.log("Error importing file {} to collection {}: {}".format(getattr(d, "id", "?"), collection_id, e), callback)
546
+ return items
547
+
548
+ def import_collections_files_collections(self, callback: Optional[callable] = None) -> int:
549
+ """
550
+ Import all documents for all collections into DB.
551
+ """
552
+ col_ids = self.get_collections_ids_collections([])
553
+ total = 0
554
+ for cid in col_ids:
555
+ items = []
556
+ try:
557
+ items = self.import_collection_files_collections(cid, items, callback=callback)
558
+ except Exception as e:
559
+ self.log("Error importing collection {} files: {}".format(cid, e), callback)
560
+ total += len(items)
561
+ return total
562
+
563
+ def upload_to_collection_collections(
564
+ self,
565
+ collection_id: str,
566
+ path: str,
567
+ fields: Optional[Dict[str, str]] = None
568
+ ):
569
+ """
570
+ Upload a file and attach to a collection (single-step helper).
571
+ """
572
+ client = self.get_client()
573
+ if not os.path.exists(path):
574
+ return None
575
+
576
+ content_type = mimetypes.guess_type(path)[0] or "application/octet-stream"
577
+ with open(path, "rb") as f:
578
+ data = f.read()
579
+
580
+ return client.collections.upload_document(
581
+ collection_id=collection_id,
582
+ name=os.path.basename(path),
583
+ data=data,
584
+ # content_type=content_type, # TODO: check if supported???
585
+ fields=fields or None,
586
+ )
587
+
588
+ def get_collection_stats_collections(self, collection_id: str) -> Dict[str, Any]:
589
+ """
590
+ Compute simple stats for a collection: total docs and total bytes.
591
+ """
592
+ count = 0
593
+ total_bytes = 0
594
+ docs = self.list_collection_documents_raw_(collection_id, limit=1000)
595
+ for d in docs:
596
+ count += 1
597
+ try:
598
+ fid = getattr(d, "id", None) or getattr(d, "file_id", None)
599
+ if not fid and isinstance(d, dict):
600
+ fid = d.get("id") or d.get("file_id")
601
+ meta = self.get_file(fid) if fid else None
602
+ size = None
603
+ if meta is not None:
604
+ size = getattr(meta, "size_bytes", None)
605
+ if size is None and hasattr(meta, "size"):
606
+ size = getattr(meta, "size")
607
+ total_bytes += int(size or 0)
608
+ except Exception:
609
+ pass
610
+ return {"count": count, "total_bytes": total_bytes}