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,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
|