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.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 .
|
|
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)
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
9
|
+
# Updated Date: 2026.01.06 20:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
|
-
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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.
|
|
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
|
|
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)
|