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.
- pygpt_net/CHANGELOG.txt +7 -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/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/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 +9 -6
- pygpt_net/data/config/models.json +5 -4
- pygpt_net/data/config/settings.json +54 -1
- 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 +4 -3
- pygpt_net/data/locale/locale.en.ini +14 -4
- pygpt_net/data/locale/locale.es.ini +4 -3
- pygpt_net/data/locale/locale.fr.ini +4 -3
- pygpt_net/data/locale/locale.it.ini +4 -3
- pygpt_net/data/locale/locale.pl.ini +5 -4
- pygpt_net/data/locale/locale.uk.ini +4 -3
- pygpt_net/data/locale/locale.zh.ini +4 -3
- pygpt_net/icons.qrc +4 -0
- pygpt_net/icons_rc.py +282 -138
- pygpt_net/provider/api/anthropic/__init__.py +2 -0
- pygpt_net/provider/api/anthropic/chat.py +84 -1
- pygpt_net/provider/api/anthropic/store.py +307 -0
- pygpt_net/provider/api/anthropic/stream.py +75 -0
- pygpt_net/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 +59 -2
- pygpt_net/provider/api/google/store.py +124 -3
- pygpt_net/provider/api/google/stream.py +91 -24
- pygpt_net/provider/api/google/worker/importer.py +16 -28
- pygpt_net/provider/api/openai/assistants.py +2 -2
- pygpt_net/provider/api/openai/store.py +4 -1
- 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 +30 -6
- 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/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_tools.py +19 -1
- pygpt_net/provider/api/x_ai/store.py +610 -0
- pygpt_net/provider/api/x_ai/stream.py +30 -9
- 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 +18 -3
- pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +2 -2
- pygpt_net/provider/core/model/patch.py +13 -0
- pygpt_net/tools/image_viewer/tool.py +334 -34
- pygpt_net/tools/image_viewer/ui/dialogs.py +317 -21
- 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/{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-2.7.7.dist-info → pygpt_net-2.7.8.dist-info}/METADATA +9 -2
- {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.8.dist-info}/RECORD +82 -70
- 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.7.dist-info → pygpt_net-2.7.8.dist-info}/LICENSE +0 -0
- {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.8.dist-info}/WHEEL +0 -0
- {pygpt_net-2.7.7.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}
|
|
@@ -69,10 +69,8 @@ def _extract_http_urls_from_text(text: Optional[str]) -> List[str]:
|
|
|
69
69
|
"""
|
|
70
70
|
if not text or not isinstance(text, str):
|
|
71
71
|
return []
|
|
72
|
-
# Basic, conservative URL regex
|
|
73
72
|
pattern = re.compile(r"(https?://[^\s)>\]\"']+)", re.IGNORECASE)
|
|
74
73
|
urls = pattern.findall(text)
|
|
75
|
-
# Deduplicate while preserving order
|
|
76
74
|
out, seen = [], set()
|
|
77
75
|
for u in urls:
|
|
78
76
|
if u not in seen:
|
|
@@ -134,6 +132,7 @@ def _process_message_content_for_outputs(core, ctx, state, content):
|
|
|
134
132
|
- If image_url.url is data:... -> save to file and append to state.image_paths + ctx.images
|
|
135
133
|
- If image_url.url is http(s) -> append to ctx.urls
|
|
136
134
|
- Extract URLs from adjacent text parts conservatively
|
|
135
|
+
- If file part present -> auto-download via Files API
|
|
137
136
|
"""
|
|
138
137
|
if not isinstance(content, list):
|
|
139
138
|
return
|
|
@@ -162,7 +161,28 @@ def _process_message_content_for_outputs(core, ctx, state, content):
|
|
|
162
161
|
urls = _extract_http_urls_from_text(t)
|
|
163
162
|
if urls:
|
|
164
163
|
_append_urls(ctx, state, urls)
|
|
165
|
-
|
|
164
|
+
elif ptype == "file":
|
|
165
|
+
fid = p.get("id") or p.get("file_id")
|
|
166
|
+
if isinstance(fid, str):
|
|
167
|
+
if not hasattr(state, "xai_downloaded_file_ids"):
|
|
168
|
+
state.xai_downloaded_file_ids = set()
|
|
169
|
+
if fid not in state.xai_downloaded_file_ids:
|
|
170
|
+
try:
|
|
171
|
+
path = core.api.xai.store.download_to_dir(fid)
|
|
172
|
+
except Exception:
|
|
173
|
+
path = None
|
|
174
|
+
if path:
|
|
175
|
+
if not isinstance(ctx.files, list):
|
|
176
|
+
ctx.files = []
|
|
177
|
+
if path not in ctx.files:
|
|
178
|
+
ctx.files.append(path)
|
|
179
|
+
ext = path.lower().rsplit(".", 1)[-1] if "." in path else ""
|
|
180
|
+
if ext in ["png", "jpg", "jpeg", "gif", "bmp", "tiff", "webp"]:
|
|
181
|
+
if not isinstance(ctx.images, list):
|
|
182
|
+
ctx.images = []
|
|
183
|
+
if path not in ctx.images:
|
|
184
|
+
ctx.images.append(path)
|
|
185
|
+
state.xai_downloaded_file_ids.add(fid)
|
|
166
186
|
if any_image:
|
|
167
187
|
try:
|
|
168
188
|
state.has_xai_inline_image = True
|
|
@@ -364,7 +384,6 @@ def process_xai_sdk_chunk(ctx, core, state, item) -> Optional[str]:
|
|
|
364
384
|
if hasattr(chunk, "content"):
|
|
365
385
|
t = _stringify_content(getattr(chunk, "content"))
|
|
366
386
|
if t:
|
|
367
|
-
# collect URLs from text content conservatively
|
|
368
387
|
_append_urls(ctx, state, _extract_http_urls_from_text(t))
|
|
369
388
|
return str(t)
|
|
370
389
|
except Exception:
|
|
@@ -425,11 +444,8 @@ def process_xai_sdk_chunk(ctx, core, state, item) -> Optional[str]:
|
|
|
425
444
|
if "content" in m and m["content"] is not None:
|
|
426
445
|
mc = m["content"]
|
|
427
446
|
# inspect for image_url outputs and URLs
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
_append_urls(ctx, state, _extract_http_urls_from_text(mc))
|
|
431
|
-
return mc
|
|
432
|
-
elif isinstance(mc, list):
|
|
447
|
+
if isinstance(mc, list):
|
|
448
|
+
_process_message_content_for_outputs(core, ctx, state, mc)
|
|
433
449
|
out_parts: List[str] = []
|
|
434
450
|
for p in mc:
|
|
435
451
|
if isinstance(p, dict) and p.get("type") == "text":
|
|
@@ -440,6 +456,11 @@ def process_xai_sdk_chunk(ctx, core, state, item) -> Optional[str]:
|
|
|
440
456
|
txt = "".join(out_parts)
|
|
441
457
|
_append_urls(ctx, state, _extract_http_urls_from_text(txt))
|
|
442
458
|
return txt
|
|
459
|
+
else:
|
|
460
|
+
t = _stringify_content(mc)
|
|
461
|
+
if t:
|
|
462
|
+
_append_urls(ctx, state, _extract_http_urls_from_text(t))
|
|
463
|
+
return str(t)
|
|
443
464
|
|
|
444
465
|
# root-level delta/message
|
|
445
466
|
if isinstance(chunk.get("delta"), dict) and "content" in chunk["delta"]:
|