pygpt-net 2.7.5__py3-none-any.whl → 2.7.7__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 +14 -0
- pygpt_net/__init__.py +4 -4
- 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} +20 -64
- pygpt_net/controller/debug/fixtures.py +3 -2
- pygpt_net/controller/files/files.py +65 -4
- pygpt_net/core/debug/models.py +2 -2
- pygpt_net/core/filesystem/url.py +4 -1
- pygpt_net/core/render/web/body.py +3 -2
- pygpt_net/core/types/chunk.py +27 -0
- pygpt_net/data/config/config.json +14 -4
- pygpt_net/data/config/models.json +192 -4
- pygpt_net/data/config/settings.json +126 -36
- pygpt_net/data/js/app/template.js +1 -1
- pygpt_net/data/js/app.min.js +2 -2
- pygpt_net/data/locale/locale.de.ini +5 -0
- pygpt_net/data/locale/locale.en.ini +35 -8
- pygpt_net/data/locale/locale.es.ini +5 -0
- pygpt_net/data/locale/locale.fr.ini +5 -0
- pygpt_net/data/locale/locale.it.ini +5 -0
- pygpt_net/data/locale/locale.pl.ini +5 -0
- pygpt_net/data/locale/locale.uk.ini +5 -0
- pygpt_net/data/locale/locale.zh.ini +5 -0
- pygpt_net/data/locale/plugin.cmd_mouse_control.en.ini +2 -2
- pygpt_net/item/ctx.py +3 -5
- pygpt_net/js_rc.py +2449 -2447
- pygpt_net/plugin/cmd_mouse_control/config.py +8 -7
- pygpt_net/plugin/cmd_mouse_control/plugin.py +3 -4
- 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 +16 -9
- pygpt_net/provider/api/anthropic/chat.py +259 -11
- pygpt_net/provider/api/anthropic/computer.py +844 -0
- pygpt_net/provider/api/anthropic/remote_tools.py +172 -0
- pygpt_net/{controller/chat/handler/anthropic_stream.py → provider/api/anthropic/stream.py} +24 -10
- pygpt_net/provider/api/anthropic/tools.py +32 -77
- pygpt_net/provider/api/anthropic/utils.py +30 -0
- pygpt_net/provider/api/google/__init__.py +6 -5
- pygpt_net/provider/api/google/chat.py +3 -8
- pygpt_net/{controller/chat/handler/google_stream.py → provider/api/google/stream.py} +1 -1
- pygpt_net/provider/api/google/utils.py +185 -0
- pygpt_net/{controller/chat/handler → 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/__init__.py +7 -3
- pygpt_net/provider/api/openai/image.py +2 -2
- pygpt_net/provider/api/openai/responses.py +0 -0
- 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/x_ai/__init__.py +117 -17
- pygpt_net/provider/api/x_ai/chat.py +272 -102
- pygpt_net/provider/api/x_ai/image.py +149 -47
- pygpt_net/provider/api/x_ai/{remote.py → remote_tools.py} +165 -70
- pygpt_net/provider/api/x_ai/responses.py +507 -0
- pygpt_net/provider/api/x_ai/stream.py +715 -0
- 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/core/config/patch.py +22 -1
- pygpt_net/provider/core/model/patch.py +26 -1
- pygpt_net/tools/image_viewer/ui/dialogs.py +300 -13
- pygpt_net/tools/text_editor/ui/dialogs.py +3 -2
- pygpt_net/tools/text_editor/ui/widgets.py +5 -1
- pygpt_net/ui/base/context_menu.py +44 -1
- pygpt_net/ui/layout/toolbox/indexes.py +22 -19
- pygpt_net/ui/layout/toolbox/model.py +28 -5
- pygpt_net/ui/widget/dialog/base.py +16 -5
- pygpt_net/ui/widget/image/display.py +25 -8
- pygpt_net/ui/widget/tabs/output.py +9 -1
- pygpt_net/ui/widget/textarea/editor.py +14 -1
- pygpt_net/ui/widget/textarea/input.py +20 -7
- pygpt_net/ui/widget/textarea/notepad.py +24 -1
- pygpt_net/ui/widget/textarea/output.py +23 -1
- pygpt_net/ui/widget/textarea/web.py +16 -1
- {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.7.dist-info}/METADATA +16 -2
- {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.7.dist-info}/RECORD +80 -73
- pygpt_net/controller/chat/handler/xai_stream.py +0 -135
- {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.7.dist-info}/LICENSE +0 -0
- {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.7.dist-info}/WHEEL +0 -0
- {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.7.dist-info}/entry_points.txt +0 -0
|
@@ -6,13 +6,13 @@
|
|
|
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 base64
|
|
13
13
|
import datetime
|
|
14
14
|
import os
|
|
15
|
-
from typing import Optional, Dict, Any, List
|
|
15
|
+
from typing import Optional, Dict, Any, List, Iterable
|
|
16
16
|
|
|
17
17
|
import requests
|
|
18
18
|
from PySide6.QtCore import QObject, Signal, QRunnable, Slot
|
|
@@ -35,7 +35,7 @@ class Image:
|
|
|
35
35
|
sync: bool = True
|
|
36
36
|
) -> bool:
|
|
37
37
|
"""
|
|
38
|
-
Generate image(s) via xAI
|
|
38
|
+
Generate image(s) via xAI SDK image API.
|
|
39
39
|
Model: grok-2-image (or -1212 variants).
|
|
40
40
|
|
|
41
41
|
:param context: BridgeContext with prompt, model, ctx
|
|
@@ -60,7 +60,7 @@ class Image:
|
|
|
60
60
|
worker = ImageWorker()
|
|
61
61
|
worker.window = self.window
|
|
62
62
|
worker.ctx = ctx
|
|
63
|
-
worker.model = model.id or "grok-2-image"
|
|
63
|
+
worker.model = (model.id or "grok-2-image")
|
|
64
64
|
worker.input_prompt = prompt
|
|
65
65
|
worker.model_prompt = prompt_model
|
|
66
66
|
worker.system_prompt = self.window.core.prompt.get('img')
|
|
@@ -108,8 +108,10 @@ class ImageWorker(QRunnable):
|
|
|
108
108
|
self.raw = False
|
|
109
109
|
self.num = 1
|
|
110
110
|
|
|
111
|
-
#
|
|
112
|
-
|
|
111
|
+
# SDK image_format:
|
|
112
|
+
# - "base64": returns raw image bytes in SDK response (preferred for local saving)
|
|
113
|
+
# - "url": returns URL on xAI managed storage (fallback: we download)
|
|
114
|
+
self.image_format = "base64"
|
|
113
115
|
|
|
114
116
|
@Slot()
|
|
115
117
|
def run(self):
|
|
@@ -143,48 +145,35 @@ class ImageWorker(QRunnable):
|
|
|
143
145
|
|
|
144
146
|
self.signals.status.emit(trans('img.status.generating') + f": {self.input_prompt}...")
|
|
145
147
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
headers = {
|
|
153
|
-
"Authorization": f"Bearer {api_key}",
|
|
154
|
-
"Content-Type": "application/json",
|
|
155
|
-
}
|
|
156
|
-
payload = {
|
|
157
|
-
"model": self.model or "grok-2-image",
|
|
158
|
-
"prompt": self.input_prompt or "",
|
|
159
|
-
"n": max(1, min(int(self.num), 10)),
|
|
160
|
-
"response_format": "b64_json", # get base64 so we can save locally
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
r = requests.post(self.api_url, headers=headers, json=payload, timeout=180)
|
|
164
|
-
r.raise_for_status()
|
|
165
|
-
data = r.json()
|
|
166
|
-
|
|
167
|
-
images = []
|
|
168
|
-
for idx, img in enumerate((data.get("data") or [])[: self.num]):
|
|
169
|
-
b64 = img.get("b64_json")
|
|
170
|
-
if not b64:
|
|
171
|
-
# fallback: url download
|
|
172
|
-
url = img.get("url")
|
|
173
|
-
if url:
|
|
174
|
-
try:
|
|
175
|
-
rr = requests.get(url, timeout=60)
|
|
176
|
-
if rr.status_code == 200:
|
|
177
|
-
images.append(rr.content)
|
|
178
|
-
except Exception:
|
|
179
|
-
pass
|
|
180
|
-
continue
|
|
181
|
-
try:
|
|
182
|
-
images.append(base64.b64decode(b64))
|
|
183
|
-
except Exception:
|
|
184
|
-
continue
|
|
148
|
+
# use xAI SDK client
|
|
149
|
+
client = self.window.core.api.xai.get_client()
|
|
150
|
+
|
|
151
|
+
# enforce n range [1..10] as per docs
|
|
152
|
+
n = max(1, min(int(self.num or 1), 10))
|
|
185
153
|
|
|
154
|
+
images_bytes: List[bytes] = []
|
|
155
|
+
if n == 1:
|
|
156
|
+
# single image
|
|
157
|
+
resp = client.image.sample(
|
|
158
|
+
model=self.model or "grok-2-image",
|
|
159
|
+
prompt=self.input_prompt or "",
|
|
160
|
+
image_format=("base64" if self.image_format == "base64" else "url"),
|
|
161
|
+
)
|
|
162
|
+
images_bytes = self._extract_bytes_from_single(resp)
|
|
163
|
+
else:
|
|
164
|
+
# batch images
|
|
165
|
+
resp_iter = client.image.sample_batch(
|
|
166
|
+
model=self.model or "grok-2-image",
|
|
167
|
+
prompt=self.input_prompt or "",
|
|
168
|
+
n=n,
|
|
169
|
+
image_format=("base64" if self.image_format == "base64" else "url"),
|
|
170
|
+
)
|
|
171
|
+
images_bytes = self._extract_bytes_from_batch(resp_iter)
|
|
172
|
+
|
|
173
|
+
# save images to files
|
|
186
174
|
paths: List[str] = []
|
|
187
|
-
for i, content in enumerate(
|
|
175
|
+
for i, content in enumerate(images_bytes):
|
|
176
|
+
# generate filename
|
|
188
177
|
name = (
|
|
189
178
|
datetime.date.today().strftime("%Y-%m-%d") + "_" +
|
|
190
179
|
datetime.datetime.now().strftime("%H-%M-%S") + "-" +
|
|
@@ -192,7 +181,7 @@ class ImageWorker(QRunnable):
|
|
|
192
181
|
str(i + 1) + ".jpg"
|
|
193
182
|
)
|
|
194
183
|
path = os.path.join(self.window.core.config.get_user_dir("img"), name)
|
|
195
|
-
self.signals.status.emit(trans('img.status.downloading') + f" ({i + 1} / {
|
|
184
|
+
self.signals.status.emit(trans('img.status.downloading') + f" ({i + 1} / {len(images_bytes)}) -> {path}")
|
|
196
185
|
|
|
197
186
|
if self.window.core.image.save_image(path, content):
|
|
198
187
|
paths.append(path)
|
|
@@ -207,6 +196,119 @@ class ImageWorker(QRunnable):
|
|
|
207
196
|
finally:
|
|
208
197
|
self._cleanup()
|
|
209
198
|
|
|
199
|
+
# ---------- SDK response helpers ----------
|
|
200
|
+
|
|
201
|
+
def _extract_bytes_from_single(self, resp) -> List[bytes]:
|
|
202
|
+
"""
|
|
203
|
+
Normalize single-image SDK response to a list of bytes.
|
|
204
|
+
Accepts:
|
|
205
|
+
- resp.image -> bytes or base64 str (docs say raw bytes)
|
|
206
|
+
- resp.url -> download
|
|
207
|
+
- dict-like/legacy: {'b64_json': ..., 'url': ...}
|
|
208
|
+
"""
|
|
209
|
+
out: List[bytes] = []
|
|
210
|
+
try:
|
|
211
|
+
# preferred path: raw bytes when image_format="base64"
|
|
212
|
+
img_bytes = getattr(resp, "image", None)
|
|
213
|
+
if isinstance(img_bytes, (bytes, bytearray)):
|
|
214
|
+
out.append(bytes(img_bytes))
|
|
215
|
+
return out
|
|
216
|
+
if isinstance(img_bytes, str):
|
|
217
|
+
try:
|
|
218
|
+
out.append(base64.b64decode(img_bytes))
|
|
219
|
+
return out
|
|
220
|
+
except Exception:
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
# url fallback
|
|
224
|
+
url = getattr(resp, "url", None)
|
|
225
|
+
if isinstance(url, str) and url:
|
|
226
|
+
try:
|
|
227
|
+
r = requests.get(url, timeout=60)
|
|
228
|
+
if r.status_code == 200:
|
|
229
|
+
out.append(r.content)
|
|
230
|
+
return out
|
|
231
|
+
except Exception:
|
|
232
|
+
pass
|
|
233
|
+
|
|
234
|
+
# dict-like fallbacks
|
|
235
|
+
if isinstance(resp, dict):
|
|
236
|
+
if "b64_json" in resp and resp["b64_json"]:
|
|
237
|
+
try:
|
|
238
|
+
out.append(base64.b64decode(resp["b64_json"]))
|
|
239
|
+
return out
|
|
240
|
+
except Exception:
|
|
241
|
+
pass
|
|
242
|
+
if "url" in resp and resp["url"]:
|
|
243
|
+
try:
|
|
244
|
+
r = requests.get(resp["url"], timeout=60)
|
|
245
|
+
if r.status_code == 200:
|
|
246
|
+
out.append(r.content)
|
|
247
|
+
return out
|
|
248
|
+
except Exception:
|
|
249
|
+
pass
|
|
250
|
+
except Exception:
|
|
251
|
+
pass
|
|
252
|
+
return out
|
|
253
|
+
|
|
254
|
+
def _extract_bytes_from_batch(self, resp_iter: Iterable) -> List[bytes]:
|
|
255
|
+
"""
|
|
256
|
+
Normalize batch SDK response (iterable of images) to a list of bytes.
|
|
257
|
+
Handles item.image (bytes/str), item.url, dict-like or bytes directly.
|
|
258
|
+
"""
|
|
259
|
+
out: List[bytes] = []
|
|
260
|
+
if resp_iter is None:
|
|
261
|
+
return out
|
|
262
|
+
try:
|
|
263
|
+
for item in resp_iter:
|
|
264
|
+
# bytes directly
|
|
265
|
+
if isinstance(item, (bytes, bytearray)):
|
|
266
|
+
out.append(bytes(item))
|
|
267
|
+
continue
|
|
268
|
+
|
|
269
|
+
# preferred: raw bytes in item.image
|
|
270
|
+
img_bytes = getattr(item, "image", None)
|
|
271
|
+
if isinstance(img_bytes, (bytes, bytearray)):
|
|
272
|
+
out.append(bytes(img_bytes))
|
|
273
|
+
continue
|
|
274
|
+
if isinstance(img_bytes, str):
|
|
275
|
+
try:
|
|
276
|
+
out.append(base64.b64decode(img_bytes))
|
|
277
|
+
continue
|
|
278
|
+
except Exception:
|
|
279
|
+
pass
|
|
280
|
+
|
|
281
|
+
# url fallback
|
|
282
|
+
url = getattr(item, "url", None)
|
|
283
|
+
if isinstance(url, str) and url:
|
|
284
|
+
try:
|
|
285
|
+
r = requests.get(url, timeout=60)
|
|
286
|
+
if r.status_code == 200:
|
|
287
|
+
out.append(r.content)
|
|
288
|
+
continue
|
|
289
|
+
except Exception:
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
# dict-like fallbacks
|
|
293
|
+
if isinstance(item, dict):
|
|
294
|
+
if "b64_json" in item and item["b64_json"]:
|
|
295
|
+
try:
|
|
296
|
+
out.append(base64.b64decode(item["b64_json"]))
|
|
297
|
+
continue
|
|
298
|
+
except Exception:
|
|
299
|
+
pass
|
|
300
|
+
if "url" in item and item["url"]:
|
|
301
|
+
try:
|
|
302
|
+
r = requests.get(item["url"], timeout=60)
|
|
303
|
+
if r.status_code == 200:
|
|
304
|
+
out.append(r.content)
|
|
305
|
+
continue
|
|
306
|
+
except Exception:
|
|
307
|
+
pass
|
|
308
|
+
except Exception:
|
|
309
|
+
pass
|
|
310
|
+
return out
|
|
311
|
+
|
|
210
312
|
def _cleanup(self):
|
|
211
313
|
"""Cleanup signals to avoid multiple calls."""
|
|
212
314
|
sig = self.signals
|
|
@@ -6,27 +6,176 @@
|
|
|
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
|
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
13
15
|
from typing import Optional, Dict, Any, List
|
|
14
16
|
|
|
15
17
|
from pygpt_net.item.model import ModelItem
|
|
16
18
|
|
|
19
|
+
# xAI server-side tools
|
|
20
|
+
try:
|
|
21
|
+
from xai_sdk.tools import web_search as x_web_search
|
|
22
|
+
from xai_sdk.tools import x_search as x_x_search
|
|
23
|
+
from xai_sdk.tools import code_execution as x_code_execution
|
|
24
|
+
from xai_sdk.tools import mcp as x_mcp
|
|
25
|
+
except Exception:
|
|
26
|
+
x_web_search = None
|
|
27
|
+
x_x_search = None
|
|
28
|
+
x_code_execution = None
|
|
29
|
+
x_mcp = None
|
|
30
|
+
|
|
17
31
|
|
|
18
32
|
class Remote:
|
|
19
33
|
def __init__(self, window=None):
|
|
20
34
|
"""
|
|
21
|
-
Live Search builder for xAI:
|
|
35
|
+
Live Search builder for xAI (old, grok-3):
|
|
22
36
|
- Returns a dict with 'sdk' (X-only toggle) and 'http' (full search_parameters).
|
|
23
37
|
- SDK path: native streaming (xai_sdk.chat.stream()) works only for basic X search (no advanced filters).
|
|
24
38
|
- HTTP path: full Live Search (web/news/rss/X with filters), streaming via SSE.
|
|
25
39
|
|
|
40
|
+
Server-side Tools builder for xAI (newer SDKs, Responses API):
|
|
41
|
+
- Builds xAI SDK tool objects for web_search, x_search, code_execution, MCP.
|
|
42
|
+
- Returns include flags, max_turns and use_encrypted_content settings.
|
|
43
|
+
|
|
26
44
|
:param window: Window instance
|
|
27
45
|
"""
|
|
28
46
|
self.window = window
|
|
29
47
|
|
|
48
|
+
def build_for_chat(self, model: ModelItem = None, stream: bool = False) -> Dict[str, Any]:
|
|
49
|
+
"""
|
|
50
|
+
Build server-side tools and options for Chat Responses.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
{
|
|
54
|
+
"tools": [ ... xai_sdk.tools.* ... ],
|
|
55
|
+
"include": [ ... ],
|
|
56
|
+
"use_encrypted_content": bool,
|
|
57
|
+
"max_turns": Optional[int],
|
|
58
|
+
}
|
|
59
|
+
"""
|
|
60
|
+
cfg = self.window.core.config
|
|
61
|
+
include: List[str] = []
|
|
62
|
+
tools: List[object] = []
|
|
63
|
+
|
|
64
|
+
# global remote tools switch
|
|
65
|
+
is_web_enabled = self.window.controller.chat.remote_tools.enabled(model, "web_search")
|
|
66
|
+
is_x_enabled = bool(cfg.get("remote_tools.xai.x_search", False))
|
|
67
|
+
is_code_enabled = bool(cfg.get("remote_tools.xai.code_execution", False))
|
|
68
|
+
is_mcp_enabled = bool(cfg.get("remote_tools.xai.mcp", False))
|
|
69
|
+
|
|
70
|
+
# include flags
|
|
71
|
+
if stream:
|
|
72
|
+
include.append("verbose_streaming")
|
|
73
|
+
if bool(cfg.get("remote_tools.xai.inline_citations", True)):
|
|
74
|
+
include.append("inline_citations")
|
|
75
|
+
if bool(cfg.get("remote_tools.xai.include_code_output", True)):
|
|
76
|
+
include.append("code_execution_call_output")
|
|
77
|
+
|
|
78
|
+
# use_encrypted_content
|
|
79
|
+
use_encrypted = bool(cfg.get("remote_tools.xai.use_encrypted_content", False))
|
|
80
|
+
|
|
81
|
+
# optional max_turns
|
|
82
|
+
max_turns = None
|
|
83
|
+
try:
|
|
84
|
+
mt = cfg.get("remote_tools.xai.max_turns")
|
|
85
|
+
if isinstance(mt, int) and mt > 0:
|
|
86
|
+
max_turns = int(mt)
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
# WEB SEARCH
|
|
91
|
+
if is_web_enabled and x_web_search is not None:
|
|
92
|
+
kwargs: Dict[str, Any] = {}
|
|
93
|
+
enable_img = bool(cfg.get("remote_tools.xai.web.enable_image_understanding", False))
|
|
94
|
+
'''
|
|
95
|
+
allowed = self._as_list(cfg.get("remote_tools.xai.web.allowed_websites"), 5)
|
|
96
|
+
excluded = self._as_list(cfg.get("remote_tools.xai.web.excluded_websites"), 5)
|
|
97
|
+
if allowed and not excluded:
|
|
98
|
+
kwargs["allowed_domains"] = allowed
|
|
99
|
+
elif excluded and not allowed:
|
|
100
|
+
kwargs["excluded_domains"] = excluded
|
|
101
|
+
'''
|
|
102
|
+
if enable_img:
|
|
103
|
+
kwargs["enable_image_understanding"] = True
|
|
104
|
+
try:
|
|
105
|
+
tools.append(x_web_search(**kwargs))
|
|
106
|
+
except Exception:
|
|
107
|
+
tools.append(x_web_search())
|
|
108
|
+
|
|
109
|
+
# X SEARCH
|
|
110
|
+
if is_x_enabled and x_x_search is not None:
|
|
111
|
+
kwargs: Dict[str, Any] = {}
|
|
112
|
+
'''
|
|
113
|
+
inc = self._as_list(cfg.get("remote_tools.xai.x.included_handles"), 10)
|
|
114
|
+
exc = self._as_list(cfg.get("remote_tools.xai.x.excluded_handles"), 10)
|
|
115
|
+
if inc and not exc:
|
|
116
|
+
kwargs["allowed_x_handles"] = inc
|
|
117
|
+
elif exc and not inc:
|
|
118
|
+
kwargs["excluded_x_handles"] = exc
|
|
119
|
+
'''
|
|
120
|
+
# optional date range filters (YYYY-MM-DD)
|
|
121
|
+
for k_in, k_out in (("remote_tools.xai.from_date", "from_date"),
|
|
122
|
+
("remote_tools.xai.to_date", "to_date")):
|
|
123
|
+
v = cfg.get(k_in)
|
|
124
|
+
if isinstance(v, str) and v.strip():
|
|
125
|
+
kwargs[k_out] = v.strip()
|
|
126
|
+
|
|
127
|
+
if bool(cfg.get("remote_tools.xai.x.enable_image_understanding", False)):
|
|
128
|
+
kwargs["enable_image_understanding"] = True
|
|
129
|
+
if bool(cfg.get("remote_tools.xai.x.enable_video_understanding", False)):
|
|
130
|
+
kwargs["enable_video_understanding"] = True
|
|
131
|
+
|
|
132
|
+
# optional favorites/views filters (supported by live search)
|
|
133
|
+
try:
|
|
134
|
+
favs = cfg.get("remote_tools.xai.x.min_favs")
|
|
135
|
+
if isinstance(favs, int) and favs > 0:
|
|
136
|
+
kwargs["post_favorite_count"] = int(favs)
|
|
137
|
+
except Exception:
|
|
138
|
+
pass
|
|
139
|
+
try:
|
|
140
|
+
views = cfg.get("remote_tools.xai.x.min_views")
|
|
141
|
+
if isinstance(views, int) and views > 0:
|
|
142
|
+
kwargs["post_view_count"] = int(views)
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
tools.append(x_x_search(**kwargs))
|
|
148
|
+
except Exception:
|
|
149
|
+
tools.append(x_x_search())
|
|
150
|
+
|
|
151
|
+
# CODE EXECUTION
|
|
152
|
+
if is_code_enabled and x_code_execution is not None:
|
|
153
|
+
try:
|
|
154
|
+
tools.append(x_code_execution())
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
# MCP
|
|
159
|
+
if is_mcp_enabled and x_mcp is not None:
|
|
160
|
+
kwargs = {}
|
|
161
|
+
mcp_config = cfg.get("remote_tools.xai.mcp.args", "")
|
|
162
|
+
if mcp_config:
|
|
163
|
+
try:
|
|
164
|
+
kwargs = json.loads(mcp_config)
|
|
165
|
+
except Exception:
|
|
166
|
+
pass
|
|
167
|
+
try:
|
|
168
|
+
tools.append(x_mcp(**kwargs))
|
|
169
|
+
except Exception:
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
"tools": tools,
|
|
174
|
+
"include": include,
|
|
175
|
+
"use_encrypted_content": use_encrypted,
|
|
176
|
+
"max_turns": max_turns,
|
|
177
|
+
}
|
|
178
|
+
|
|
30
179
|
def build_remote_tools(self, model: ModelItem = None) -> Dict[str, Any]:
|
|
31
180
|
"""
|
|
32
181
|
Return live-search config for xAI:
|
|
@@ -58,38 +207,17 @@ class Remote:
|
|
|
58
207
|
"""
|
|
59
208
|
cfg = self.window.core.config
|
|
60
209
|
is_web = self.window.controller.chat.remote_tools.enabled(model, "web_search") # get global config
|
|
61
|
-
|
|
62
|
-
mode = str(cfg.get("remote_tools.xai.mode") or "auto").lower()
|
|
63
|
-
if mode not in ("auto", "on", "off"):
|
|
64
|
-
mode = "auto"
|
|
65
|
-
|
|
66
|
-
if mode == "off":
|
|
67
|
-
if is_web:
|
|
68
|
-
mode = "on" # override off if global web_search enabled
|
|
210
|
+
mode = "on" if is_web else "off"
|
|
69
211
|
|
|
70
212
|
# sources toggles
|
|
71
|
-
s_web =
|
|
72
|
-
s_x =
|
|
73
|
-
s_news = bool(cfg.get("remote_tools.xai.sources.news", False))
|
|
74
|
-
s_rss = bool(cfg.get("remote_tools.xai.sources.rss", False))
|
|
75
|
-
|
|
76
|
-
# advanced flags
|
|
77
|
-
adv_web_allowed = self._has_list(cfg.get("remote_tools.xai.web.allowed_websites"))
|
|
78
|
-
adv_web_excluded = self._has_list(cfg.get("remote_tools.xai.web.excluded_websites"))
|
|
79
|
-
adv_web_country = self._has_str(cfg.get("remote_tools.xai.web.country"))
|
|
80
|
-
adv_web_safe = cfg.get("remote_tools.xai.web.safe_search", None)
|
|
81
|
-
|
|
82
|
-
adv_news_excl = self._has_list(cfg.get("remote_tools.xai.news.excluded_websites"))
|
|
83
|
-
adv_news_country = self._has_str(cfg.get("remote_tools.xai.news.country"))
|
|
84
|
-
adv_news_safe = cfg.get("remote_tools.xai.news.safe_search", None)
|
|
213
|
+
s_web = bool(cfg.get("remote_tools.xai.web_search", False))
|
|
214
|
+
s_x = bool(cfg.get("remote_tools.xai.x_search", False))
|
|
85
215
|
|
|
86
216
|
adv_x_incl = self._has_list(cfg.get("remote_tools.xai.x.included_handles"))
|
|
87
217
|
adv_x_excl = self._has_list(cfg.get("remote_tools.xai.x.excluded_handles"))
|
|
88
218
|
adv_x_favs = self._has_int(cfg.get("remote_tools.xai.x.min_favs"))
|
|
89
219
|
adv_x_views = self._has_int(cfg.get("remote_tools.xai.x.min_views"))
|
|
90
220
|
|
|
91
|
-
adv_rss_link = self._has_str(cfg.get("remote_tools.xai.rss.link"))
|
|
92
|
-
|
|
93
221
|
adv_from = self._has_str(cfg.get("remote_tools.xai.from_date"))
|
|
94
222
|
adv_to = self._has_str(cfg.get("remote_tools.xai.to_date"))
|
|
95
223
|
|
|
@@ -97,7 +225,7 @@ class Remote:
|
|
|
97
225
|
adv_return_cits = cfg.get("remote_tools.xai.return_citations", True) is not True # different than default?
|
|
98
226
|
|
|
99
227
|
# SDK-capable if: mode!=off and ONLY X is enabled and no X filters/date/max_results customizations
|
|
100
|
-
x_only = s_x and not s_web
|
|
228
|
+
x_only = s_x and not s_web
|
|
101
229
|
x_filters = any([adv_x_incl, adv_x_excl, adv_x_favs, adv_x_views])
|
|
102
230
|
sdk_enabled = (mode != "off") and x_only and not any([
|
|
103
231
|
x_filters, adv_from, adv_to, adv_max_results, adv_return_cits
|
|
@@ -110,10 +238,10 @@ class Remote:
|
|
|
110
238
|
|
|
111
239
|
need_http = (mode != "off") and (
|
|
112
240
|
not sdk_enabled or # advanced X filters or other sources/date/results/citations
|
|
113
|
-
s_web
|
|
241
|
+
s_web
|
|
114
242
|
)
|
|
115
243
|
if need_http:
|
|
116
|
-
http_params = self._build_http_params(cfg, mode, s_web, s_x
|
|
244
|
+
http_params = self._build_http_params(cfg, mode, s_web, s_x)
|
|
117
245
|
http_reason = "advanced_sources_or_filters"
|
|
118
246
|
|
|
119
247
|
return {
|
|
@@ -131,8 +259,6 @@ class Remote:
|
|
|
131
259
|
mode: str,
|
|
132
260
|
s_web: bool,
|
|
133
261
|
s_x: bool,
|
|
134
|
-
s_news: bool,
|
|
135
|
-
s_rss: bool
|
|
136
262
|
) -> dict:
|
|
137
263
|
"""
|
|
138
264
|
Build search_parameters for Chat Completions (HTTP).
|
|
@@ -141,8 +267,6 @@ class Remote:
|
|
|
141
267
|
:param mode: "auto"|"on"|"off"
|
|
142
268
|
:param s_web: Include web search
|
|
143
269
|
:param s_x: Include X search
|
|
144
|
-
:param s_news: Include news search
|
|
145
|
-
:param s_rss: Include RSS search
|
|
146
270
|
:return: search_parameters dict
|
|
147
271
|
"""
|
|
148
272
|
params: Dict[str, Any] = {"mode": mode}
|
|
@@ -196,41 +320,12 @@ class Remote:
|
|
|
196
320
|
xsrc["post_view_count"] = int(views)
|
|
197
321
|
sources.append(xsrc)
|
|
198
322
|
|
|
199
|
-
if s_news:
|
|
200
|
-
news: Dict[str, Any] = {"type": "news"}
|
|
201
|
-
country = cfg.get("remote_tools.xai.news.country")
|
|
202
|
-
if isinstance(country, str) and len(country.strip()) == 2:
|
|
203
|
-
news["country"] = country.strip().upper()
|
|
204
|
-
excluded = self._as_list(cfg.get("remote_tools.xai.news.excluded_websites"), 5)
|
|
205
|
-
if excluded:
|
|
206
|
-
news["excluded_websites"] = excluded
|
|
207
|
-
safe = cfg.get("remote_tools.xai.news.safe_search")
|
|
208
|
-
if safe is not None:
|
|
209
|
-
news["safe_search"] = bool(safe)
|
|
210
|
-
sources.append(news)
|
|
211
|
-
|
|
212
|
-
if s_rss:
|
|
213
|
-
link = cfg.get("remote_tools.xai.rss.link")
|
|
214
|
-
if isinstance(link, str) and link.strip():
|
|
215
|
-
sources.append({"type": "rss", "links": [link.strip()]})
|
|
216
|
-
|
|
217
323
|
if sources:
|
|
218
324
|
params["sources"] = sources
|
|
219
325
|
|
|
220
326
|
return params
|
|
221
327
|
|
|
222
|
-
|
|
223
|
-
"""
|
|
224
|
-
Return True if v is a non-empty list/tuple or a non-empty comma-separated string.
|
|
225
|
-
|
|
226
|
-
:param v: Any
|
|
227
|
-
:return: bool
|
|
228
|
-
"""
|
|
229
|
-
if v is None:
|
|
230
|
-
return False
|
|
231
|
-
if isinstance(v, (list, tuple)):
|
|
232
|
-
return len([x for x in v if str(x).strip()]) > 0
|
|
233
|
-
return len([x for x in str(v).split(",") if x.strip()]) > 0
|
|
328
|
+
# ---------- helpers ----------
|
|
234
329
|
|
|
235
330
|
def _has_str(self, v) -> bool:
|
|
236
331
|
"""
|
|
@@ -250,14 +345,14 @@ class Remote:
|
|
|
250
345
|
"""
|
|
251
346
|
return isinstance(v, int) and v > 0
|
|
252
347
|
|
|
253
|
-
def
|
|
254
|
-
|
|
255
|
-
|
|
348
|
+
def _has_list(self, v) -> bool:
|
|
349
|
+
if v is None:
|
|
350
|
+
return False
|
|
351
|
+
if isinstance(v, (list, tuple)):
|
|
352
|
+
return len([x for x in v if str(x).strip()]) > 0
|
|
353
|
+
return len([x for x in str(v).split(",") if x.strip()]) > 0
|
|
256
354
|
|
|
257
|
-
|
|
258
|
-
:param limit: limit number of items
|
|
259
|
-
:return: List of strings
|
|
260
|
-
"""
|
|
355
|
+
def _as_list(self, v, limit: int) -> List[str]:
|
|
261
356
|
if v is None:
|
|
262
357
|
return []
|
|
263
358
|
if isinstance(v, (list, tuple)):
|