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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2026.01.
|
|
9
|
+
# Updated Date: 2026.01.05 20:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from typing import Optional, List, Dict, Any
|
|
@@ -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
|
|
@@ -296,9 +316,20 @@ def process_xai_sdk_chunk(ctx, core, state, item) -> Optional[str]:
|
|
|
296
316
|
except Exception:
|
|
297
317
|
return None
|
|
298
318
|
|
|
319
|
+
# persist last response and attach response id to ctx once
|
|
299
320
|
try:
|
|
300
321
|
if response is not None:
|
|
301
322
|
state.xai_last_response = response
|
|
323
|
+
rid = getattr(response, "id", None)
|
|
324
|
+
if rid and not getattr(ctx, "msg_id", None):
|
|
325
|
+
ctx.msg_id = str(rid)
|
|
326
|
+
if not isinstance(ctx.extra, dict):
|
|
327
|
+
ctx.extra = {}
|
|
328
|
+
ctx.extra["xai_response_id"] = ctx.msg_id
|
|
329
|
+
try:
|
|
330
|
+
core.ctx.update_item(ctx)
|
|
331
|
+
except Exception:
|
|
332
|
+
pass
|
|
302
333
|
except Exception:
|
|
303
334
|
pass
|
|
304
335
|
|
|
@@ -353,7 +384,6 @@ def process_xai_sdk_chunk(ctx, core, state, item) -> Optional[str]:
|
|
|
353
384
|
if hasattr(chunk, "content"):
|
|
354
385
|
t = _stringify_content(getattr(chunk, "content"))
|
|
355
386
|
if t:
|
|
356
|
-
# collect URLs from text content conservatively
|
|
357
387
|
_append_urls(ctx, state, _extract_http_urls_from_text(t))
|
|
358
388
|
return str(t)
|
|
359
389
|
except Exception:
|
|
@@ -414,11 +444,8 @@ def process_xai_sdk_chunk(ctx, core, state, item) -> Optional[str]:
|
|
|
414
444
|
if "content" in m and m["content"] is not None:
|
|
415
445
|
mc = m["content"]
|
|
416
446
|
# inspect for image_url outputs and URLs
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
_append_urls(ctx, state, _extract_http_urls_from_text(mc))
|
|
420
|
-
return mc
|
|
421
|
-
elif isinstance(mc, list):
|
|
447
|
+
if isinstance(mc, list):
|
|
448
|
+
_process_message_content_for_outputs(core, ctx, state, mc)
|
|
422
449
|
out_parts: List[str] = []
|
|
423
450
|
for p in mc:
|
|
424
451
|
if isinstance(p, dict) and p.get("type") == "text":
|
|
@@ -429,6 +456,11 @@ def process_xai_sdk_chunk(ctx, core, state, item) -> Optional[str]:
|
|
|
429
456
|
txt = "".join(out_parts)
|
|
430
457
|
_append_urls(ctx, state, _extract_http_urls_from_text(txt))
|
|
431
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)
|
|
432
464
|
|
|
433
465
|
# root-level delta/message
|
|
434
466
|
if isinstance(chunk.get("delta"), dict) and "content" in chunk["delta"]:
|
|
@@ -6,20 +6,26 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date:
|
|
9
|
+
# Updated Date: 2026.01.04 19:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import json
|
|
13
13
|
from typing import List, Any, Optional
|
|
14
14
|
|
|
15
|
+
# xAI SDK client-side tool descriptor
|
|
16
|
+
try:
|
|
17
|
+
from xai_sdk.chat import tool as x_tool
|
|
18
|
+
except Exception:
|
|
19
|
+
x_tool = None
|
|
20
|
+
|
|
15
21
|
|
|
16
22
|
class Tools:
|
|
17
23
|
def __init__(self, window=None):
|
|
18
24
|
"""
|
|
19
|
-
Tools mapper for xAI
|
|
25
|
+
Tools mapper for xAI.
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
- prepare(): legacy OpenAI-compatible dicts (kept for compatibility if needed).
|
|
28
|
+
- prepare_sdk_tools(): xAI SDK client-side tool descriptors for Chat Responses.
|
|
23
29
|
|
|
24
30
|
:param window: Window instance
|
|
25
31
|
"""
|
|
@@ -84,12 +90,10 @@ class Tools:
|
|
|
84
90
|
|
|
85
91
|
def prepare(self, functions: list) -> List[dict]:
|
|
86
92
|
"""
|
|
87
|
-
Prepare xAI tools list
|
|
88
|
-
|
|
89
|
-
Returns [] if no functions provided.
|
|
93
|
+
Prepare legacy xAI/OpenAI-compatible tools list from app functions list.
|
|
90
94
|
|
|
91
95
|
:param functions: List of functions with keys: name (str), desc (str), params (JSON Schema str)
|
|
92
|
-
:return: List of tools
|
|
96
|
+
:return: List of tools in dict format
|
|
93
97
|
"""
|
|
94
98
|
if not functions or not isinstance(functions, list):
|
|
95
99
|
return []
|
|
@@ -117,4 +121,51 @@ class Tools:
|
|
|
117
121
|
"description": desc,
|
|
118
122
|
"parameters": params,
|
|
119
123
|
})
|
|
124
|
+
return tools
|
|
125
|
+
|
|
126
|
+
def prepare_sdk_tools(self, functions: list) -> List[object]:
|
|
127
|
+
"""
|
|
128
|
+
Prepare xAI SDK client-side tool descriptors for Chat Responses.
|
|
129
|
+
|
|
130
|
+
:param functions: List of functions with keys: name (str), desc (str), params (JSON Schema str)
|
|
131
|
+
:return: List of xai_sdk.chat.tool(...) objects
|
|
132
|
+
"""
|
|
133
|
+
if x_tool is None:
|
|
134
|
+
return [] # SDK too old; skip silently
|
|
135
|
+
if not functions or not isinstance(functions, list):
|
|
136
|
+
return []
|
|
137
|
+
|
|
138
|
+
tools: List[object] = []
|
|
139
|
+
for fn in functions:
|
|
140
|
+
name = str(fn.get("name") or "").strip()
|
|
141
|
+
if not name:
|
|
142
|
+
continue
|
|
143
|
+
desc = fn.get("desc") or ""
|
|
144
|
+
params: Optional[dict] = {}
|
|
145
|
+
if fn.get("params"):
|
|
146
|
+
try:
|
|
147
|
+
params = json.loads(fn["params"])
|
|
148
|
+
except Exception:
|
|
149
|
+
params = {}
|
|
150
|
+
params = self._sanitize_schema(params or {})
|
|
151
|
+
if not params.get("type"):
|
|
152
|
+
params["type"] = "object"
|
|
153
|
+
else:
|
|
154
|
+
params = {"type": "object"}
|
|
155
|
+
try:
|
|
156
|
+
tools.append(x_tool(
|
|
157
|
+
name=name,
|
|
158
|
+
description=desc,
|
|
159
|
+
parameters=params,
|
|
160
|
+
))
|
|
161
|
+
except Exception:
|
|
162
|
+
# In case of schema issues, fallback to empty-params tool
|
|
163
|
+
try:
|
|
164
|
+
tools.append(x_tool(
|
|
165
|
+
name=name,
|
|
166
|
+
description=desc,
|
|
167
|
+
parameters={"type": "object"},
|
|
168
|
+
))
|
|
169
|
+
except Exception:
|
|
170
|
+
continue
|
|
120
171
|
return tools
|
|
@@ -6,10 +6,9 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date:
|
|
9
|
+
# Updated Date: 2026.01.05 20:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
|
-
import base64
|
|
13
12
|
from typing import Any, Optional
|
|
14
13
|
|
|
15
14
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date:
|
|
9
|
+
# Updated Date: 2026.01.04 19:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import os
|
|
@@ -50,9 +50,6 @@ class Vision:
|
|
|
50
50
|
try:
|
|
51
51
|
if att.path and self.window.core.api.xai.vision.is_image(att.path):
|
|
52
52
|
mime = self.window.core.api.xai.vision.guess_mime(att.path)
|
|
53
|
-
# Accept only JPEG/PNG for SDK too (for consistency)
|
|
54
|
-
#if mime not in self.allowed_mimes:
|
|
55
|
-
# continue
|
|
56
53
|
with open(att.path, "rb") as f:
|
|
57
54
|
b64 = base64.b64encode(f.read()).decode("utf-8")
|
|
58
55
|
images.append(f"data:{mime};base64,{b64}")
|
|
@@ -0,0 +1,308 @@
|
|
|
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
|
+
|
|
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 (xAI Collections)
|
|
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
|
+
# ---------- Vector stores (Collections) ----------
|
|
69
|
+
|
|
70
|
+
def import_vector_stores(self):
|
|
71
|
+
"""Import collections"""
|
|
72
|
+
self.worker = ImportWorker()
|
|
73
|
+
self.worker.window = self.window
|
|
74
|
+
self.worker.mode = "vector_stores"
|
|
75
|
+
self.connect_signals(self.worker)
|
|
76
|
+
self.window.threadpool.start(self.worker)
|
|
77
|
+
|
|
78
|
+
def truncate_vector_stores(self):
|
|
79
|
+
"""Delete collections"""
|
|
80
|
+
self.worker = ImportWorker()
|
|
81
|
+
self.worker.window = self.window
|
|
82
|
+
self.worker.mode = "truncate_vector_stores"
|
|
83
|
+
self.connect_signals(self.worker)
|
|
84
|
+
self.window.threadpool.start(self.worker)
|
|
85
|
+
|
|
86
|
+
def refresh_vector_stores(self):
|
|
87
|
+
"""Refresh collections"""
|
|
88
|
+
self.worker = ImportWorker()
|
|
89
|
+
self.worker.window = self.window
|
|
90
|
+
self.worker.mode = "refresh_vector_stores"
|
|
91
|
+
self.connect_signals(self.worker)
|
|
92
|
+
self.window.threadpool.start(self.worker)
|
|
93
|
+
|
|
94
|
+
# ---------- Files (documents) ----------
|
|
95
|
+
|
|
96
|
+
def truncate_files(self, store_id: str = None):
|
|
97
|
+
"""Remove documents from one/all collections"""
|
|
98
|
+
self.worker = ImportWorker()
|
|
99
|
+
self.worker.window = self.window
|
|
100
|
+
self.worker.mode = "truncate_files"
|
|
101
|
+
self.worker.store_id = store_id
|
|
102
|
+
self.connect_signals(self.worker)
|
|
103
|
+
self.window.threadpool.start(self.worker)
|
|
104
|
+
|
|
105
|
+
def upload_files(self, store_id: str, files: list = None):
|
|
106
|
+
"""Upload files to a collection"""
|
|
107
|
+
self.worker = ImportWorker()
|
|
108
|
+
self.worker.window = self.window
|
|
109
|
+
self.worker.mode = "upload_files"
|
|
110
|
+
self.worker.store_id = store_id
|
|
111
|
+
self.worker.files = files or []
|
|
112
|
+
self.connect_signals(self.worker)
|
|
113
|
+
self.window.threadpool.start(self.worker)
|
|
114
|
+
|
|
115
|
+
def import_files(self, store_id: str = None):
|
|
116
|
+
"""Import documents from one/all collections"""
|
|
117
|
+
self.worker = ImportWorker()
|
|
118
|
+
self.worker.window = self.window
|
|
119
|
+
self.worker.mode = "import_files"
|
|
120
|
+
self.worker.store_id = store_id
|
|
121
|
+
self.connect_signals(self.worker)
|
|
122
|
+
self.window.threadpool.start(self.worker)
|
|
123
|
+
|
|
124
|
+
def connect_signals(self, worker):
|
|
125
|
+
worker.signals.finished.connect(self.handle_finished)
|
|
126
|
+
worker.signals.error.connect(self.handle_error)
|
|
127
|
+
worker.signals.status.connect(self.handle_status)
|
|
128
|
+
worker.signals.log.connect(self.handle_log)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ImportWorkerSignals(QObject):
|
|
132
|
+
status = Signal(str, str) # mode, message
|
|
133
|
+
finished = Signal(str, str, int) # mode, store_id, num
|
|
134
|
+
error = Signal(str, object) # mode, error
|
|
135
|
+
log = Signal(str, str) # mode, message
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class ImportWorker(QRunnable):
|
|
139
|
+
"""Import worker (xAI Collections)"""
|
|
140
|
+
def __init__(self, *args, **kwargs):
|
|
141
|
+
super().__init__()
|
|
142
|
+
self.signals = ImportWorkerSignals()
|
|
143
|
+
self.window = None
|
|
144
|
+
self.mode = "vector_stores"
|
|
145
|
+
self.store_id = None
|
|
146
|
+
self.files = []
|
|
147
|
+
|
|
148
|
+
@Slot()
|
|
149
|
+
def run(self):
|
|
150
|
+
try:
|
|
151
|
+
if self.mode == "vector_stores":
|
|
152
|
+
if self.import_vector_stores():
|
|
153
|
+
self.import_files()
|
|
154
|
+
elif self.mode == "truncate_vector_stores":
|
|
155
|
+
self.truncate_vector_stores()
|
|
156
|
+
elif self.mode == "refresh_vector_stores":
|
|
157
|
+
self.refresh_vector_stores()
|
|
158
|
+
elif self.mode == "truncate_files":
|
|
159
|
+
self.truncate_files()
|
|
160
|
+
elif self.mode == "import_files":
|
|
161
|
+
self.import_files()
|
|
162
|
+
elif self.mode == "upload_files":
|
|
163
|
+
self.upload_files()
|
|
164
|
+
except Exception as e:
|
|
165
|
+
self.signals.error.emit(self.mode, e)
|
|
166
|
+
finally:
|
|
167
|
+
self.cleanup()
|
|
168
|
+
|
|
169
|
+
# ---------- Collections ----------
|
|
170
|
+
|
|
171
|
+
def import_vector_stores(self, silent: bool = False) -> bool:
|
|
172
|
+
try:
|
|
173
|
+
self.log("Importing collections...")
|
|
174
|
+
self.window.core.remote_store.xai.clear()
|
|
175
|
+
items = {}
|
|
176
|
+
self.window.core.api.xai.store.import_collections_collections(items, callback=self.callback)
|
|
177
|
+
self.window.core.remote_store.xai.import_items(items)
|
|
178
|
+
if not silent:
|
|
179
|
+
self.signals.finished.emit("vector_stores", self.store_id, len(items))
|
|
180
|
+
return True
|
|
181
|
+
except Exception as e:
|
|
182
|
+
self.log("API error: {}".format(e))
|
|
183
|
+
self.signals.error.emit("vector_stores", e)
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
def truncate_vector_stores(self, silent: bool = False) -> bool:
|
|
187
|
+
try:
|
|
188
|
+
self.log("Truncating collections...")
|
|
189
|
+
num = self.window.core.api.xai.store.remove_all_collections_collections(callback=self.callback)
|
|
190
|
+
self.window.core.remote_store.xai.items = {}
|
|
191
|
+
self.window.core.remote_store.xai.save()
|
|
192
|
+
if not silent:
|
|
193
|
+
self.signals.finished.emit("truncate_vector_stores", self.store_id, num)
|
|
194
|
+
return True
|
|
195
|
+
except Exception as e:
|
|
196
|
+
self.log("API error: {}".format(e))
|
|
197
|
+
self.signals.error.emit("truncate_vector_stores", e)
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
def refresh_vector_stores(self, silent: bool = False) -> bool:
|
|
201
|
+
try:
|
|
202
|
+
self.log("Refreshing collections...")
|
|
203
|
+
num = 0
|
|
204
|
+
stores = self.window.core.remote_store.xai.items
|
|
205
|
+
for id in list(stores.keys()):
|
|
206
|
+
store = stores[id]
|
|
207
|
+
try:
|
|
208
|
+
self.window.controller.remote_store.refresh_store(store, update=False, provider="xai")
|
|
209
|
+
num += 1
|
|
210
|
+
except Exception as e:
|
|
211
|
+
self.log("Failed to refresh collection: {}".format(id))
|
|
212
|
+
self.window.core.debug.log(e)
|
|
213
|
+
if not silent:
|
|
214
|
+
self.signals.finished.emit("refresh_vector_stores", self.store_id, num)
|
|
215
|
+
return True
|
|
216
|
+
except Exception as e:
|
|
217
|
+
self.log("API error: {}".format(e))
|
|
218
|
+
self.signals.error.emit("refresh_vector_stores", e)
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
# ---------- Documents ----------
|
|
222
|
+
|
|
223
|
+
def truncate_files(self, silent: bool = False) -> bool:
|
|
224
|
+
try:
|
|
225
|
+
if self.store_id is None:
|
|
226
|
+
self.log("Truncating all collection documents...")
|
|
227
|
+
self.window.core.remote_store.xai.files.truncate() # clear all local + detach from all collections
|
|
228
|
+
num = self.window.core.api.xai.store.remove_files(callback=self.callback) # delete remote files
|
|
229
|
+
else:
|
|
230
|
+
self.log("Truncating documents for collection: {}".format(self.store_id))
|
|
231
|
+
self.window.core.remote_store.xai.files.truncate(self.store_id) # clear local + detach from this collection
|
|
232
|
+
num = self.window.core.api.xai.store.remove_from_collection_collections(
|
|
233
|
+
self.store_id,
|
|
234
|
+
callback=self.callback,
|
|
235
|
+
)
|
|
236
|
+
if not silent:
|
|
237
|
+
self.signals.finished.emit("truncate_files", self.store_id, num)
|
|
238
|
+
return True
|
|
239
|
+
except Exception as e:
|
|
240
|
+
self.log("API error: {}".format(e))
|
|
241
|
+
self.signals.error.emit("truncate_files", e)
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
def upload_files(self, silent: bool = False) -> bool:
|
|
245
|
+
num = 0
|
|
246
|
+
try:
|
|
247
|
+
self.log("Uploading files to collection...")
|
|
248
|
+
for path in self.files:
|
|
249
|
+
try:
|
|
250
|
+
doc = self.window.core.api.xai.store.upload_to_collection_collections(self.store_id, path)
|
|
251
|
+
if doc is not None:
|
|
252
|
+
self.window.core.remote_store.xai.files.insert(self.store_id, doc.file_metadata)
|
|
253
|
+
num += 1
|
|
254
|
+
msg = "Uploaded file: {}/{}".format(num, len(self.files))
|
|
255
|
+
self.signals.status.emit("upload_files", msg)
|
|
256
|
+
self.log(msg)
|
|
257
|
+
else:
|
|
258
|
+
self.signals.status.emit("upload_files", "Failed to upload: {}".format(os.path.basename(path)))
|
|
259
|
+
except Exception as e:
|
|
260
|
+
self.window.core.debug.log(e)
|
|
261
|
+
self.signals.status.emit("upload_files", "Failed to upload: {}".format(os.path.basename(path)))
|
|
262
|
+
if not silent:
|
|
263
|
+
self.signals.finished.emit("upload_files", self.store_id, num)
|
|
264
|
+
return True
|
|
265
|
+
except Exception as e:
|
|
266
|
+
self.log("API error: {}".format(e))
|
|
267
|
+
self.signals.error.emit("upload_files", e)
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
def import_files(self, silent: bool = False) -> bool:
|
|
271
|
+
try:
|
|
272
|
+
if self.store_id is None:
|
|
273
|
+
self.log("Importing all collection documents...")
|
|
274
|
+
self.window.core.remote_store.xai.files.truncate_local() # clear local DB (all)
|
|
275
|
+
num = self.window.core.api.xai.store.import_collections_files_collections(callback=self.callback)
|
|
276
|
+
else:
|
|
277
|
+
self.log("Importing documents for collection: {}".format(self.store_id))
|
|
278
|
+
self.window.core.remote_store.xai.files.truncate_local(self.store_id) # clear local DB (store)
|
|
279
|
+
items = self.window.core.api.xai.store.import_collection_files_collections(
|
|
280
|
+
self.store_id,
|
|
281
|
+
[],
|
|
282
|
+
callback=self.callback,
|
|
283
|
+
)
|
|
284
|
+
num = len(items)
|
|
285
|
+
if not silent:
|
|
286
|
+
self.signals.finished.emit("import_files", self.store_id, num)
|
|
287
|
+
return True
|
|
288
|
+
except Exception as e:
|
|
289
|
+
self.log("API error: {}".format(e))
|
|
290
|
+
self.signals.error.emit("import_files", e)
|
|
291
|
+
return False
|
|
292
|
+
|
|
293
|
+
# ---------- Utils ----------
|
|
294
|
+
|
|
295
|
+
def callback(self, msg: str):
|
|
296
|
+
self.log(msg)
|
|
297
|
+
|
|
298
|
+
def log(self, msg: str):
|
|
299
|
+
self.signals.log.emit(self.mode, msg)
|
|
300
|
+
|
|
301
|
+
def cleanup(self):
|
|
302
|
+
sig = self.signals
|
|
303
|
+
self.signals = None
|
|
304
|
+
if sig is not None:
|
|
305
|
+
try:
|
|
306
|
+
sig.deleteLater()
|
|
307
|
+
except RuntimeError:
|
|
308
|
+
pass
|