pygpt-net 2.7.6__py3-none-any.whl → 2.7.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. pygpt_net/CHANGELOG.txt +13 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +5 -1
  4. pygpt_net/controller/assistant/batch.py +2 -2
  5. pygpt_net/controller/assistant/files.py +7 -6
  6. pygpt_net/controller/assistant/threads.py +0 -0
  7. pygpt_net/controller/chat/command.py +0 -0
  8. pygpt_net/controller/chat/remote_tools.py +3 -9
  9. pygpt_net/controller/chat/stream.py +2 -2
  10. pygpt_net/controller/chat/{handler/worker.py → stream_worker.py} +13 -35
  11. pygpt_net/controller/dialogs/confirm.py +35 -58
  12. pygpt_net/controller/lang/mapping.py +9 -9
  13. pygpt_net/controller/remote_store/{google/batch.py → batch.py} +209 -252
  14. pygpt_net/controller/remote_store/remote_store.py +982 -13
  15. pygpt_net/core/command/command.py +0 -0
  16. pygpt_net/core/db/viewer.py +1 -1
  17. pygpt_net/core/debug/models.py +2 -2
  18. pygpt_net/core/realtime/worker.py +3 -1
  19. pygpt_net/{controller/remote_store/google → core/remote_store/anthropic}/__init__.py +0 -1
  20. pygpt_net/core/remote_store/anthropic/files.py +211 -0
  21. pygpt_net/core/remote_store/anthropic/store.py +208 -0
  22. pygpt_net/core/remote_store/openai/store.py +5 -4
  23. pygpt_net/core/remote_store/remote_store.py +5 -1
  24. pygpt_net/{controller/remote_store/openai → core/remote_store/xai}/__init__.py +0 -1
  25. pygpt_net/core/remote_store/xai/files.py +225 -0
  26. pygpt_net/core/remote_store/xai/store.py +219 -0
  27. pygpt_net/data/config/config.json +18 -5
  28. pygpt_net/data/config/models.json +193 -4
  29. pygpt_net/data/config/settings.json +179 -36
  30. pygpt_net/data/icons/folder_eye.svg +1 -0
  31. pygpt_net/data/icons/folder_eye_filled.svg +1 -0
  32. pygpt_net/data/icons/folder_open.svg +1 -0
  33. pygpt_net/data/icons/folder_open_filled.svg +1 -0
  34. pygpt_net/data/locale/locale.de.ini +6 -3
  35. pygpt_net/data/locale/locale.en.ini +46 -12
  36. pygpt_net/data/locale/locale.es.ini +6 -3
  37. pygpt_net/data/locale/locale.fr.ini +6 -3
  38. pygpt_net/data/locale/locale.it.ini +6 -3
  39. pygpt_net/data/locale/locale.pl.ini +7 -4
  40. pygpt_net/data/locale/locale.uk.ini +6 -3
  41. pygpt_net/data/locale/locale.zh.ini +6 -3
  42. pygpt_net/icons.qrc +4 -0
  43. pygpt_net/icons_rc.py +282 -138
  44. pygpt_net/plugin/cmd_mouse_control/worker.py +2 -1
  45. pygpt_net/plugin/cmd_mouse_control/worker_sandbox.py +2 -1
  46. pygpt_net/provider/api/anthropic/__init__.py +10 -3
  47. pygpt_net/provider/api/anthropic/chat.py +342 -11
  48. pygpt_net/provider/api/anthropic/computer.py +844 -0
  49. pygpt_net/provider/api/anthropic/remote_tools.py +172 -0
  50. pygpt_net/provider/api/anthropic/store.py +307 -0
  51. pygpt_net/{controller/chat/handler/anthropic_stream.py → provider/api/anthropic/stream.py} +99 -10
  52. pygpt_net/provider/api/anthropic/tools.py +32 -77
  53. pygpt_net/provider/api/anthropic/utils.py +30 -0
  54. pygpt_net/{controller/chat/handler → provider/api/anthropic/worker}/__init__.py +0 -0
  55. pygpt_net/provider/api/anthropic/worker/importer.py +278 -0
  56. pygpt_net/provider/api/google/chat.py +62 -9
  57. pygpt_net/provider/api/google/store.py +124 -3
  58. pygpt_net/{controller/chat/handler/google_stream.py → provider/api/google/stream.py} +92 -25
  59. pygpt_net/provider/api/google/utils.py +185 -0
  60. pygpt_net/provider/api/google/worker/importer.py +16 -28
  61. pygpt_net/provider/api/langchain/__init__.py +0 -0
  62. pygpt_net/{controller/chat/handler/langchain_stream.py → provider/api/langchain/stream.py} +1 -1
  63. pygpt_net/provider/api/llama_index/__init__.py +0 -0
  64. pygpt_net/{controller/chat/handler/llamaindex_stream.py → provider/api/llama_index/stream.py} +1 -1
  65. pygpt_net/provider/api/openai/assistants.py +2 -2
  66. pygpt_net/provider/api/openai/image.py +2 -2
  67. pygpt_net/provider/api/openai/store.py +4 -1
  68. pygpt_net/{controller/chat/handler/openai_stream.py → provider/api/openai/stream.py} +1 -1
  69. pygpt_net/provider/api/openai/utils.py +69 -3
  70. pygpt_net/provider/api/openai/worker/importer.py +19 -61
  71. pygpt_net/provider/api/openai/worker/importer_assistants.py +230 -0
  72. pygpt_net/provider/api/x_ai/__init__.py +138 -15
  73. pygpt_net/provider/api/x_ai/audio.py +43 -11
  74. pygpt_net/provider/api/x_ai/chat.py +92 -4
  75. pygpt_net/provider/api/x_ai/image.py +149 -47
  76. pygpt_net/provider/api/x_ai/realtime/__init__.py +12 -0
  77. pygpt_net/provider/api/x_ai/realtime/client.py +1825 -0
  78. pygpt_net/provider/api/x_ai/realtime/realtime.py +198 -0
  79. pygpt_net/provider/api/x_ai/{remote.py → remote_tools.py} +183 -70
  80. pygpt_net/provider/api/x_ai/responses.py +507 -0
  81. pygpt_net/provider/api/x_ai/store.py +610 -0
  82. pygpt_net/{controller/chat/handler/xai_stream.py → provider/api/x_ai/stream.py} +42 -10
  83. pygpt_net/provider/api/x_ai/tools.py +59 -8
  84. pygpt_net/{controller/chat/handler → provider/api/x_ai}/utils.py +1 -2
  85. pygpt_net/provider/api/x_ai/vision.py +1 -4
  86. pygpt_net/provider/api/x_ai/worker/importer.py +308 -0
  87. pygpt_net/provider/audio_input/xai_grok_voice.py +390 -0
  88. pygpt_net/provider/audio_output/xai_tts.py +325 -0
  89. pygpt_net/provider/core/config/patch.py +39 -3
  90. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +2 -2
  91. pygpt_net/provider/core/model/patch.py +39 -1
  92. pygpt_net/tools/image_viewer/tool.py +334 -34
  93. pygpt_net/tools/image_viewer/ui/dialogs.py +319 -22
  94. pygpt_net/tools/text_editor/ui/dialogs.py +3 -2
  95. pygpt_net/tools/text_editor/ui/widgets.py +0 -0
  96. pygpt_net/ui/dialog/assistant.py +1 -1
  97. pygpt_net/ui/dialog/plugins.py +13 -5
  98. pygpt_net/ui/dialog/remote_store.py +552 -0
  99. pygpt_net/ui/dialogs.py +3 -5
  100. pygpt_net/ui/layout/ctx/ctx_list.py +58 -7
  101. pygpt_net/ui/menu/tools.py +6 -13
  102. pygpt_net/ui/widget/dialog/base.py +16 -5
  103. pygpt_net/ui/widget/dialog/{remote_store_google.py → remote_store.py} +10 -10
  104. pygpt_net/ui/widget/element/button.py +4 -4
  105. pygpt_net/ui/widget/image/display.py +2 -2
  106. pygpt_net/ui/widget/lists/context.py +2 -2
  107. pygpt_net/ui/widget/textarea/editor.py +0 -0
  108. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.dist-info}/METADATA +15 -2
  109. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.dist-info}/RECORD +107 -89
  110. pygpt_net/controller/remote_store/google/store.py +0 -615
  111. pygpt_net/controller/remote_store/openai/batch.py +0 -524
  112. pygpt_net/controller/remote_store/openai/store.py +0 -699
  113. pygpt_net/ui/dialog/remote_store_google.py +0 -539
  114. pygpt_net/ui/dialog/remote_store_openai.py +0 -539
  115. pygpt_net/ui/widget/dialog/remote_store_openai.py +0 -56
  116. pygpt_net/ui/widget/lists/remote_store_google.py +0 -248
  117. pygpt_net/ui/widget/lists/remote_store_openai.py +0 -317
  118. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.dist-info}/LICENSE +0 -0
  119. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.dist-info}/WHEEL +0 -0
  120. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.dist-info}/entry_points.txt +0 -0
@@ -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.03 17:00:00 #
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
- # If images were added, mark flag similarly to Google path
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
- _process_message_content_for_outputs(core, ctx, state, mc if isinstance(mc, list) else [])
418
- if isinstance(mc, str):
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: 2025.09.05 01:00:00 #
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 Chat Completions-compatible schema.
25
+ Tools mapper for xAI.
20
26
 
21
- Input: app 'functions' list with keys: name, desc, params (JSON Schema string).
22
- Output: list of dicts with keys: name, description, parameters.
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 (OpenAI-compatible schema) from app functions 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 with keys: name (str), description (str), parameters (dict)
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: 2025.09.05 00:00:00 #
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: 2025.09.05 01:00:00 #
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