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.
- pygpt_net/CHANGELOG.txt +13 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +5 -1
- pygpt_net/controller/assistant/batch.py +2 -2
- pygpt_net/controller/assistant/files.py +7 -6
- pygpt_net/controller/assistant/threads.py +0 -0
- pygpt_net/controller/chat/command.py +0 -0
- pygpt_net/controller/chat/remote_tools.py +3 -9
- pygpt_net/controller/chat/stream.py +2 -2
- pygpt_net/controller/chat/{handler/worker.py → stream_worker.py} +13 -35
- pygpt_net/controller/dialogs/confirm.py +35 -58
- pygpt_net/controller/lang/mapping.py +9 -9
- pygpt_net/controller/remote_store/{google/batch.py → batch.py} +209 -252
- pygpt_net/controller/remote_store/remote_store.py +982 -13
- pygpt_net/core/command/command.py +0 -0
- pygpt_net/core/db/viewer.py +1 -1
- pygpt_net/core/debug/models.py +2 -2
- pygpt_net/core/realtime/worker.py +3 -1
- pygpt_net/{controller/remote_store/google → core/remote_store/anthropic}/__init__.py +0 -1
- pygpt_net/core/remote_store/anthropic/files.py +211 -0
- pygpt_net/core/remote_store/anthropic/store.py +208 -0
- pygpt_net/core/remote_store/openai/store.py +5 -4
- pygpt_net/core/remote_store/remote_store.py +5 -1
- pygpt_net/{controller/remote_store/openai → core/remote_store/xai}/__init__.py +0 -1
- pygpt_net/core/remote_store/xai/files.py +225 -0
- pygpt_net/core/remote_store/xai/store.py +219 -0
- pygpt_net/data/config/config.json +18 -5
- pygpt_net/data/config/models.json +193 -4
- pygpt_net/data/config/settings.json +179 -36
- pygpt_net/data/icons/folder_eye.svg +1 -0
- pygpt_net/data/icons/folder_eye_filled.svg +1 -0
- pygpt_net/data/icons/folder_open.svg +1 -0
- pygpt_net/data/icons/folder_open_filled.svg +1 -0
- pygpt_net/data/locale/locale.de.ini +6 -3
- pygpt_net/data/locale/locale.en.ini +46 -12
- pygpt_net/data/locale/locale.es.ini +6 -3
- pygpt_net/data/locale/locale.fr.ini +6 -3
- pygpt_net/data/locale/locale.it.ini +6 -3
- pygpt_net/data/locale/locale.pl.ini +7 -4
- pygpt_net/data/locale/locale.uk.ini +6 -3
- pygpt_net/data/locale/locale.zh.ini +6 -3
- pygpt_net/icons.qrc +4 -0
- pygpt_net/icons_rc.py +282 -138
- pygpt_net/plugin/cmd_mouse_control/worker.py +2 -1
- pygpt_net/plugin/cmd_mouse_control/worker_sandbox.py +2 -1
- pygpt_net/provider/api/anthropic/__init__.py +10 -3
- pygpt_net/provider/api/anthropic/chat.py +342 -11
- pygpt_net/provider/api/anthropic/computer.py +844 -0
- pygpt_net/provider/api/anthropic/remote_tools.py +172 -0
- pygpt_net/provider/api/anthropic/store.py +307 -0
- pygpt_net/{controller/chat/handler/anthropic_stream.py → provider/api/anthropic/stream.py} +99 -10
- pygpt_net/provider/api/anthropic/tools.py +32 -77
- pygpt_net/provider/api/anthropic/utils.py +30 -0
- pygpt_net/{controller/chat/handler → provider/api/anthropic/worker}/__init__.py +0 -0
- pygpt_net/provider/api/anthropic/worker/importer.py +278 -0
- pygpt_net/provider/api/google/chat.py +62 -9
- pygpt_net/provider/api/google/store.py +124 -3
- pygpt_net/{controller/chat/handler/google_stream.py → provider/api/google/stream.py} +92 -25
- pygpt_net/provider/api/google/utils.py +185 -0
- pygpt_net/provider/api/google/worker/importer.py +16 -28
- pygpt_net/provider/api/langchain/__init__.py +0 -0
- pygpt_net/{controller/chat/handler/langchain_stream.py → provider/api/langchain/stream.py} +1 -1
- pygpt_net/provider/api/llama_index/__init__.py +0 -0
- pygpt_net/{controller/chat/handler/llamaindex_stream.py → provider/api/llama_index/stream.py} +1 -1
- pygpt_net/provider/api/openai/assistants.py +2 -2
- pygpt_net/provider/api/openai/image.py +2 -2
- pygpt_net/provider/api/openai/store.py +4 -1
- pygpt_net/{controller/chat/handler/openai_stream.py → provider/api/openai/stream.py} +1 -1
- pygpt_net/provider/api/openai/utils.py +69 -3
- pygpt_net/provider/api/openai/worker/importer.py +19 -61
- pygpt_net/provider/api/openai/worker/importer_assistants.py +230 -0
- pygpt_net/provider/api/x_ai/__init__.py +138 -15
- pygpt_net/provider/api/x_ai/audio.py +43 -11
- pygpt_net/provider/api/x_ai/chat.py +92 -4
- pygpt_net/provider/api/x_ai/image.py +149 -47
- pygpt_net/provider/api/x_ai/realtime/__init__.py +12 -0
- pygpt_net/provider/api/x_ai/realtime/client.py +1825 -0
- pygpt_net/provider/api/x_ai/realtime/realtime.py +198 -0
- pygpt_net/provider/api/x_ai/{remote.py → remote_tools.py} +183 -70
- pygpt_net/provider/api/x_ai/responses.py +507 -0
- pygpt_net/provider/api/x_ai/store.py +610 -0
- pygpt_net/{controller/chat/handler/xai_stream.py → provider/api/x_ai/stream.py} +42 -10
- pygpt_net/provider/api/x_ai/tools.py +59 -8
- pygpt_net/{controller/chat/handler → provider/api/x_ai}/utils.py +1 -2
- pygpt_net/provider/api/x_ai/vision.py +1 -4
- pygpt_net/provider/api/x_ai/worker/importer.py +308 -0
- pygpt_net/provider/audio_input/xai_grok_voice.py +390 -0
- pygpt_net/provider/audio_output/xai_tts.py +325 -0
- pygpt_net/provider/core/config/patch.py +39 -3
- pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +2 -2
- pygpt_net/provider/core/model/patch.py +39 -1
- pygpt_net/tools/image_viewer/tool.py +334 -34
- pygpt_net/tools/image_viewer/ui/dialogs.py +319 -22
- pygpt_net/tools/text_editor/ui/dialogs.py +3 -2
- pygpt_net/tools/text_editor/ui/widgets.py +0 -0
- pygpt_net/ui/dialog/assistant.py +1 -1
- pygpt_net/ui/dialog/plugins.py +13 -5
- pygpt_net/ui/dialog/remote_store.py +552 -0
- pygpt_net/ui/dialogs.py +3 -5
- pygpt_net/ui/layout/ctx/ctx_list.py +58 -7
- pygpt_net/ui/menu/tools.py +6 -13
- pygpt_net/ui/widget/dialog/base.py +16 -5
- pygpt_net/ui/widget/dialog/{remote_store_google.py → remote_store.py} +10 -10
- pygpt_net/ui/widget/element/button.py +4 -4
- pygpt_net/ui/widget/image/display.py +2 -2
- pygpt_net/ui/widget/lists/context.py +2 -2
- pygpt_net/ui/widget/textarea/editor.py +0 -0
- {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.dist-info}/METADATA +15 -2
- {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.dist-info}/RECORD +107 -89
- pygpt_net/controller/remote_store/google/store.py +0 -615
- pygpt_net/controller/remote_store/openai/batch.py +0 -524
- pygpt_net/controller/remote_store/openai/store.py +0 -699
- pygpt_net/ui/dialog/remote_store_google.py +0 -539
- pygpt_net/ui/dialog/remote_store_openai.py +0 -539
- pygpt_net/ui/widget/dialog/remote_store_openai.py +0 -56
- pygpt_net/ui/widget/lists/remote_store_google.py +0 -248
- pygpt_net/ui/widget/lists/remote_store_openai.py +0 -317
- {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.dist-info}/LICENSE +0 -0
- {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.dist-info}/WHEEL +0 -0
- {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}
|