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.06 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Optional, Dict, Any
@@ -33,7 +33,10 @@ from .vision import Vision
33
33
  from .tools import Tools
34
34
  from .audio import Audio
35
35
  from .image import Image
36
- from .remote import Remote
36
+ from .remote_tools import Remote
37
+ from .responses import Responses
38
+ from .store import Store
39
+ from .realtime import Realtime
37
40
 
38
41
 
39
42
  class ApiXAI:
@@ -49,7 +52,10 @@ class ApiXAI:
49
52
  self.tools = Tools(window)
50
53
  self.audio = Audio(window)
51
54
  self.image = Image(window)
52
- self.remote = Remote(window) # Live Search builder
55
+ self.remote = Remote(window)
56
+ self.responses = Responses(window)
57
+ self.store = Store(window)
58
+ self.realtime = Realtime(window)
53
59
  self.client: Optional[xai_sdk.Client] = None
54
60
  self.locked = False
55
61
  self.last_client_args: Optional[Dict[str, Any]] = None
@@ -57,7 +63,8 @@ class ApiXAI:
57
63
  def get_client(
58
64
  self,
59
65
  mode: str = MODE_CHAT,
60
- model: ModelItem = None
66
+ model: ModelItem = None,
67
+ management_api_key = None
61
68
  ) -> xai_sdk.Client:
62
69
  """
63
70
  Get or create xAI client.
@@ -67,11 +74,9 @@ class ApiXAI:
67
74
 
68
75
  :param mode: One of MODE_*
69
76
  :param model: ModelItem (optional, not used currently)
77
+ :param management_api_key: Override API key (for management calls)
70
78
  :return: xai_sdk.Client
71
79
  """
72
- if self.client is not None:
73
- return self.client
74
-
75
80
  cfg = self.window.core.config
76
81
  api_key = cfg.get("api_key_xai") or os.environ.get("XAI_API_KEY") or ""
77
82
  timeout = cfg.get("api_native_xai.timeout") # optional
@@ -88,7 +93,13 @@ class ApiXAI:
88
93
  if proxy:
89
94
  kwargs["channel_options"] = []
90
95
  kwargs["channel_options"].append(("grpc.http_proxy", proxy))
96
+ if management_api_key:
97
+ kwargs["management_api_key"] = management_api_key
98
+
99
+ if self.client is not None and self.last_client_args == kwargs:
100
+ return self.client
91
101
 
102
+ self.last_client_args = kwargs
92
103
  self.client = xai_sdk.Client(**kwargs)
93
104
  return self.client
94
105
 
@@ -101,8 +112,9 @@ class ApiXAI:
101
112
  """
102
113
  Make an API call to xAI.
103
114
 
104
- Supports chat (stream/non-stream), images (via REST),
105
- and function-calling. Audio is not available in public xAI SDK at this time.
115
+ Uses old API and Chat Responses (stateful) via xai_sdk:
116
+ - Streaming: chat.stream() (tuples of (response, chunk))
117
+ - Non-stream: chat.sample()
106
118
 
107
119
  :param context: BridgeContext
108
120
  :param extra: Extra params (not used)
@@ -113,19 +125,45 @@ class ApiXAI:
113
125
  stream = context.stream
114
126
  ctx = context.ctx
115
127
  ai_name = (ctx.output_name if ctx else "assistant")
128
+ model = context.model # model instance (item, not id)
116
129
  used_tokens = 0
117
130
  response = None
118
131
  ctx.chunk_type = ChunkType.XAI_SDK
132
+
133
+ use_responses_api = True
134
+ if model and model.id.startswith("grok-3"):
135
+ use_responses_api = False # use old API
119
136
 
120
137
  if mode in (
121
138
  MODE_COMPLETION,
122
139
  MODE_CHAT,
123
140
  MODE_AUDIO,
124
- MODE_RESEARCH
141
+ MODE_RESEARCH,
142
+ MODE_AUDIO
125
143
  ):
126
- # There is no public realtime audio in SDK; treat MODE_AUDIO as chat (TTS not supported).
127
- response = self.chat.send(context=context, extra=extra)
128
- used_tokens = self.chat.get_used_tokens()
144
+ if mode == MODE_AUDIO:
145
+ raise NotImplementedError("Not available. xAI realtime audio streaming coming soon!")
146
+
147
+ if mode == MODE_AUDIO and stream:
148
+ # Realtime API for audio streaming
149
+ is_realtime = self.realtime.begin(
150
+ context=context,
151
+ model=model,
152
+ extra=extra or {},
153
+ rt_signals=rt_signals
154
+ )
155
+ if is_realtime:
156
+ return True
157
+
158
+ # Audio TTS is not exposed via public SDK; treat MODE_AUDIO as chat input.
159
+ # NOTE: for grok-3 use Chat completions, for > grok-4 use Chat responses
160
+ if use_responses_api:
161
+ response = self.responses.send(context=context, extra=extra) # responses
162
+ used_tokens = self.responses.get_used_tokens()
163
+ else:
164
+ response = self.chat.send(context=context, extra=extra) # completions
165
+ used_tokens = self.chat.get_used_tokens()
166
+
129
167
  if ctx:
130
168
  self.vision.append_images(ctx)
131
169
 
@@ -151,7 +189,10 @@ class ApiXAI:
151
189
 
152
190
  if ctx:
153
191
  ctx.ai_name = ai_name
154
- self.chat.unpack_response(context.mode, response, ctx)
192
+ if use_responses_api:
193
+ self.responses.unpack_response(context.mode, response, ctx)
194
+ else:
195
+ self.chat.unpack_response(context.mode, response, ctx)
155
196
  try:
156
197
  for tc in getattr(ctx, "tool_calls", []) or []:
157
198
  fn = tc.get("function") or {}
@@ -175,6 +216,88 @@ class ApiXAI:
175
216
 
176
217
  If context.request is set, makes a full call() instead (for consistency).
177
218
 
219
+ :param context: BridgeContext
220
+ :param extra: Extra params (not used)
221
+ :return: Output text or "" on error
222
+ """
223
+ model = context.model or self.window.core.models.from_defaults()
224
+ if model and model.id.startswith("grok-3"):
225
+ return self.quick_call_old(context, extra) # grok-3 uses old path
226
+
227
+ if context.request:
228
+ context.stream = False
229
+ context.mode = MODE_CHAT
230
+ self.locked = True
231
+ self.call(context, extra)
232
+ self.locked = False
233
+ return context.ctx.output
234
+
235
+ self.locked = True
236
+ try:
237
+ ctx = context.ctx
238
+ prompt = context.prompt
239
+ system_prompt = context.system_prompt
240
+ history = context.history
241
+ functions = context.external_functions
242
+ attachments = context.attachments
243
+ multimodal_ctx = context.multimodal_ctx
244
+
245
+ # Prepare client-side tools for SDK (no server-side tools in quick_call)
246
+ client_tools = self.tools.prepare_sdk_tools(functions)
247
+
248
+ client = self.get_client(MODE_CHAT, model)
249
+ # store_messages: false for quick, and false if images present (SDK guidance)
250
+ store_messages = False
251
+ prev_id = None
252
+
253
+ # Create chat session
254
+ include = []
255
+ chat = client.chat.create(
256
+ model=model.id,
257
+ tools=(client_tools if client_tools else None),
258
+ include=(include if include else None),
259
+ store_messages=store_messages,
260
+ previous_response_id=prev_id,
261
+ )
262
+
263
+ # Append history if enabled and no previous_response_id is used
264
+ self.responses.append_history_sdk(
265
+ chat=chat,
266
+ system_prompt=system_prompt,
267
+ model=model,
268
+ history=history,
269
+ )
270
+
271
+ # Append current prompt with optional images
272
+ self.responses.append_current_user_sdk(
273
+ chat=chat,
274
+ prompt=prompt,
275
+ attachments=attachments,
276
+ multimodal_ctx=multimodal_ctx,
277
+ )
278
+
279
+ resp = chat.sample()
280
+ # Extract client-side tool calls if any (leave server-side out)
281
+ out = getattr(resp, "content", "") or ""
282
+ if ctx:
283
+ self.responses.quick_collect_response_id(resp, ctx)
284
+ return out.strip()
285
+ except Exception as e:
286
+ self.window.core.debug.log(e)
287
+ return ""
288
+ finally:
289
+ self.locked = False
290
+
291
+ def quick_call_old(
292
+ self,
293
+ context: BridgeContext,
294
+ extra: dict = None
295
+ ) -> str:
296
+ """
297
+ Quick non-streaming xAI chat call and return output text.
298
+
299
+ If context.request is set, makes a full call() instead (for consistency).
300
+
178
301
  :param context: BridgeContext
179
302
  :param extra: Extra params (not used)
180
303
  :return: Output text or "" on error
@@ -202,7 +325,7 @@ class ApiXAI:
202
325
  # If tools are present, prefer non-streaming HTTP Chat Completions path to extract tool calls reliably.
203
326
  # Otherwise use native SDK chat.sample().
204
327
  if tools:
205
- out, calls, citations, usage = self.chat.call_http_nonstream(
328
+ out, calls, citations, usage = self.chat.call_http_nonstream(
206
329
  model=model.id,
207
330
  prompt=prompt,
208
331
  system_prompt=system_prompt,
@@ -6,27 +6,59 @@
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.06 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from typing import Tuple
12
+ import base64
13
+ from typing import Optional, Union, List, Dict, Any
14
+
15
+ from pygpt_net.core.bridge.context import MultimodalContext
13
16
 
14
17
 
15
18
  class Audio:
16
19
  def __init__(self, window=None):
17
20
  """
18
- Audio helpers for xAI.
19
-
20
- Note: As of now, the public xAI Python SDK does not expose TTS/STT or realtime audio APIs.
21
- This class exists to keep provider surface compatible.
21
+ Audio input wrapper
22
22
 
23
23
  :param window: Window instance
24
24
  """
25
25
  self.window = window
26
26
 
27
- # Placeholders to keep interface parity
28
- def build_part(self, multimodal_ctx) -> None:
29
- return None
27
+ def build_content(
28
+ self,
29
+ content: Optional[Union[str, list]] = None,
30
+ multimodal_ctx: Optional[MultimodalContext] = None,
31
+ ) -> List[Dict[str, Any]]:
32
+ """
33
+ Build audio content from multimodal context
34
+
35
+ :param content: previous content or input prompt
36
+ :param multimodal_ctx: multimodal context
37
+ :return: List of contents
38
+ """
39
+ if not isinstance(content, list):
40
+ if content:
41
+ content = [
42
+ {
43
+ "type": "text",
44
+ "text": str(content),
45
+ }
46
+ ]
47
+ else:
48
+ content = [] # if empty input return empty list
49
+
50
+ # abort if no audio input provided
51
+ if not multimodal_ctx.is_audio_input:
52
+ return content
30
53
 
31
- def extract_first_audio_part(self, response) -> Tuple[None, None]:
32
- return None, None
54
+ encoded = base64.b64encode(multimodal_ctx.audio_data).decode('utf-8')
55
+ audio_format = multimodal_ctx.audio_format # wav by default
56
+ audio_data = {
57
+ "type": "input_audio",
58
+ "input_audio": {
59
+ "data": encoded,
60
+ "format": audio_format,
61
+ }
62
+ }
63
+ content.append(audio_data)
64
+ return content
@@ -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 __future__ import annotations
@@ -34,7 +34,6 @@ class Chat:
34
34
  """
35
35
  self.window = window
36
36
  self.input_tokens = 0
37
- # Image constraints (can be overridden by config keys below)
38
37
  self.allowed_mimes = {"image/jpeg", "image/png"}
39
38
  self.default_image_max_bytes = 10 * 1024 * 1024 # 10 MiB default
40
39
 
@@ -319,6 +318,12 @@ class Chat:
319
318
  except Exception:
320
319
  pass
321
320
 
321
+ try:
322
+ # Attempt to auto-download file parts or references (file id)
323
+ self._maybe_download_files_from_response(response, ctx)
324
+ except Exception:
325
+ pass
326
+
322
327
  # Usage
323
328
  try:
324
329
  if isinstance(response, dict) and response.get("usage"):
@@ -1089,7 +1094,7 @@ class Chat:
1089
1094
 
1090
1095
  def _collect_images_from_message_parts(self, parts: List[dict], ctx: CtxItem):
1091
1096
  """
1092
- Inspect assistant message parts for image_url outputs and store them.
1097
+ Inspect assistant message parts for image_url outputs and URLs.
1093
1098
  For http(s) URLs -> add to ctx.urls; for data URLs -> save to file and add to ctx.images.
1094
1099
  """
1095
1100
  try:
@@ -1098,6 +1103,25 @@ class Chat:
1098
1103
  for p in parts:
1099
1104
  if not isinstance(p, dict):
1100
1105
  continue
1106
+ if p.get("type") == "file":
1107
+ file_id = p.get("id") or p.get("file_id")
1108
+ if isinstance(file_id, str):
1109
+ try:
1110
+ save = self.window.core.api.xai.store.download_to_dir(file_id)
1111
+ if save:
1112
+ if not isinstance(ctx.files, list):
1113
+ ctx.files = []
1114
+ if save not in ctx.files:
1115
+ ctx.files.append(save)
1116
+ ext = os.path.splitext(save)[1].lower().lstrip(".")
1117
+ if ext in ["png", "jpg", "jpeg", "gif", "bmp", "tiff", "webp"]:
1118
+ if not isinstance(ctx.images, list):
1119
+ ctx.images = []
1120
+ if save not in ctx.images:
1121
+ ctx.images.append(save)
1122
+ except Exception:
1123
+ pass
1124
+ continue
1101
1125
  if p.get("type") != "image_url":
1102
1126
  continue
1103
1127
  img = p.get("image_url") or {}
@@ -1135,4 +1159,68 @@ class Chat:
1135
1159
  """
1136
1160
  Return the locally estimated input tokens count.
1137
1161
  """
1138
- return self.input_tokens
1162
+ return self.input_tokens
1163
+
1164
+ def _maybe_download_files_from_response(self, response, ctx: CtxItem) -> None:
1165
+ """
1166
+ Attempt to download any files referenced by id in response payloads (dict/SDK/proto).
1167
+ """
1168
+ def _walk(o, acc: set):
1169
+ if o is None:
1170
+ return
1171
+ if isinstance(o, dict):
1172
+ fid = o.get("file_id") or o.get("id") if o.get("type") == "file" else None
1173
+ if isinstance(fid, str) and fid.startswith("file-"):
1174
+ acc.add(fid)
1175
+ for v in o.values():
1176
+ _walk(v, acc)
1177
+ elif isinstance(o, (list, tuple)):
1178
+ for it in o:
1179
+ _walk(it, acc)
1180
+
1181
+ ids = set()
1182
+ try:
1183
+ if isinstance(response, dict):
1184
+ _walk(response, ids)
1185
+ else:
1186
+ msg = getattr(response, "message", None) or getattr(response, "output_message", None)
1187
+ if msg:
1188
+ _walk(getattr(msg, "content", None), ids)
1189
+ proto = getattr(response, "proto", None)
1190
+ if proto:
1191
+ ch = getattr(proto, "choices", None) or []
1192
+ if ch:
1193
+ m = getattr(ch[0], "message", None)
1194
+ if m:
1195
+ _walk(getattr(m, "content", None), ids)
1196
+ except Exception:
1197
+ pass
1198
+
1199
+ if not ids:
1200
+ return
1201
+ saved = []
1202
+ for fid in ids:
1203
+ try:
1204
+ p = self.window.core.api.xai.store.download_to_dir(fid)
1205
+ if p:
1206
+ saved.append(p)
1207
+ except Exception:
1208
+ continue
1209
+ if saved:
1210
+ saved = self.window.core.filesystem.make_local_list(saved)
1211
+ if not isinstance(ctx.files, list):
1212
+ ctx.files = []
1213
+ for p in saved:
1214
+ if p not in ctx.files:
1215
+ ctx.files.append(p)
1216
+ imgs = []
1217
+ for p in saved:
1218
+ ext = os.path.splitext(p)[1].lower().lstrip(".")
1219
+ if ext in ["png", "jpg", "jpeg", "gif", "bmp", "tiff", "webp"]:
1220
+ imgs.append(p)
1221
+ if imgs:
1222
+ if not isinstance(ctx.images, list):
1223
+ ctx.images = []
1224
+ for p in imgs:
1225
+ if p not in ctx.images:
1226
+ ctx.images.append(p)