pygpt-net 2.6.36__py3-none-any.whl → 2.6.38__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 +12 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/controller/chat/handler/anthropic_stream.py +164 -0
- pygpt_net/controller/chat/handler/google_stream.py +181 -0
- pygpt_net/controller/chat/handler/langchain_stream.py +24 -0
- pygpt_net/controller/chat/handler/llamaindex_stream.py +47 -0
- pygpt_net/controller/chat/handler/openai_stream.py +260 -0
- pygpt_net/controller/chat/handler/utils.py +210 -0
- pygpt_net/controller/chat/handler/worker.py +570 -0
- pygpt_net/controller/chat/handler/xai_stream.py +135 -0
- pygpt_net/controller/chat/stream.py +1 -1
- pygpt_net/controller/ctx/ctx.py +1 -1
- pygpt_net/controller/debug/debug.py +6 -6
- pygpt_net/controller/model/editor.py +3 -0
- pygpt_net/controller/model/importer.py +9 -2
- pygpt_net/controller/plugins/plugins.py +11 -3
- pygpt_net/controller/presets/presets.py +2 -2
- pygpt_net/core/bridge/context.py +35 -35
- pygpt_net/core/bridge/worker.py +40 -16
- pygpt_net/core/ctx/bag.py +7 -2
- pygpt_net/core/ctx/reply.py +17 -2
- pygpt_net/core/db/viewer.py +19 -34
- pygpt_net/core/render/plain/pid.py +12 -1
- pygpt_net/core/render/web/body.py +30 -39
- pygpt_net/core/tabs/tab.py +24 -1
- pygpt_net/data/config/config.json +10 -3
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/settings.json +105 -0
- pygpt_net/data/css/style.dark.css +2 -3
- pygpt_net/data/css/style.light.css +2 -3
- pygpt_net/data/locale/locale.de.ini +3 -1
- pygpt_net/data/locale/locale.en.ini +19 -1
- pygpt_net/data/locale/locale.es.ini +3 -1
- pygpt_net/data/locale/locale.fr.ini +3 -1
- pygpt_net/data/locale/locale.it.ini +3 -1
- pygpt_net/data/locale/locale.pl.ini +4 -2
- pygpt_net/data/locale/locale.uk.ini +3 -1
- pygpt_net/data/locale/locale.zh.ini +3 -1
- pygpt_net/item/assistant.py +51 -2
- pygpt_net/item/attachment.py +21 -20
- pygpt_net/item/calendar_note.py +19 -2
- pygpt_net/item/ctx.py +115 -2
- pygpt_net/item/index.py +9 -2
- pygpt_net/item/mode.py +9 -6
- pygpt_net/item/model.py +20 -3
- pygpt_net/item/notepad.py +14 -2
- pygpt_net/item/preset.py +42 -2
- pygpt_net/item/prompt.py +8 -2
- pygpt_net/plugin/cmd_files/plugin.py +2 -2
- pygpt_net/provider/api/__init__.py +5 -3
- pygpt_net/provider/api/anthropic/__init__.py +190 -29
- pygpt_net/provider/api/anthropic/audio.py +30 -0
- pygpt_net/provider/api/anthropic/chat.py +341 -0
- pygpt_net/provider/api/anthropic/image.py +25 -0
- pygpt_net/provider/api/anthropic/tools.py +266 -0
- pygpt_net/provider/api/anthropic/vision.py +142 -0
- pygpt_net/provider/api/google/chat.py +2 -2
- pygpt_net/provider/api/google/realtime/client.py +2 -2
- pygpt_net/provider/api/google/tools.py +58 -48
- pygpt_net/provider/api/google/vision.py +7 -1
- pygpt_net/provider/api/openai/chat.py +1 -0
- pygpt_net/provider/api/openai/vision.py +6 -0
- pygpt_net/provider/api/x_ai/__init__.py +247 -0
- pygpt_net/provider/api/x_ai/audio.py +32 -0
- pygpt_net/provider/api/x_ai/chat.py +968 -0
- pygpt_net/provider/api/x_ai/image.py +208 -0
- pygpt_net/provider/api/x_ai/remote.py +262 -0
- pygpt_net/provider/api/x_ai/tools.py +120 -0
- pygpt_net/provider/api/x_ai/vision.py +119 -0
- pygpt_net/provider/core/attachment/json_file.py +2 -2
- pygpt_net/provider/core/config/patch.py +28 -0
- pygpt_net/provider/llms/anthropic.py +4 -2
- pygpt_net/tools/text_editor/tool.py +4 -1
- pygpt_net/tools/text_editor/ui/dialogs.py +1 -1
- pygpt_net/ui/base/config_dialog.py +5 -11
- pygpt_net/ui/dialog/db.py +177 -59
- pygpt_net/ui/dialog/dictionary.py +57 -59
- pygpt_net/ui/dialog/editor.py +3 -2
- pygpt_net/ui/dialog/image.py +1 -1
- pygpt_net/ui/dialog/logger.py +3 -2
- pygpt_net/ui/dialog/models.py +16 -16
- pygpt_net/ui/dialog/plugins.py +63 -60
- pygpt_net/ui/layout/ctx/ctx_list.py +3 -4
- pygpt_net/ui/layout/toolbox/__init__.py +2 -2
- pygpt_net/ui/layout/toolbox/assistants.py +8 -9
- pygpt_net/ui/layout/toolbox/presets.py +2 -2
- pygpt_net/ui/main.py +9 -4
- pygpt_net/ui/widget/element/labels.py +20 -4
- pygpt_net/ui/widget/textarea/editor.py +0 -4
- pygpt_net/ui/widget/textarea/web.py +1 -1
- {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/METADATA +18 -6
- {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/RECORD +95 -76
- pygpt_net/controller/chat/handler/stream_worker.py +0 -1136
- {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
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: 2025.09.05 01:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
import base64
|
|
13
|
+
import datetime
|
|
14
|
+
import os
|
|
15
|
+
from typing import Optional, Dict, Any, List
|
|
16
|
+
|
|
17
|
+
import requests
|
|
18
|
+
from PySide6.QtCore import QObject, Signal, QRunnable, Slot
|
|
19
|
+
|
|
20
|
+
from pygpt_net.core.events import KernelEvent
|
|
21
|
+
from pygpt_net.core.bridge.context import BridgeContext
|
|
22
|
+
from pygpt_net.item.ctx import CtxItem
|
|
23
|
+
from pygpt_net.utils import trans
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Image:
|
|
27
|
+
def __init__(self, window=None):
|
|
28
|
+
self.window = window
|
|
29
|
+
self.worker = None
|
|
30
|
+
|
|
31
|
+
def generate(
|
|
32
|
+
self,
|
|
33
|
+
context: BridgeContext,
|
|
34
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
35
|
+
sync: bool = True
|
|
36
|
+
) -> bool:
|
|
37
|
+
"""
|
|
38
|
+
Generate image(s) via xAI REST API /v1/images/generations (OpenAI-compatible).
|
|
39
|
+
Model: grok-2-image (or -1212 variants).
|
|
40
|
+
|
|
41
|
+
:param context: BridgeContext with prompt, model, ctx
|
|
42
|
+
:param extra: Extra parameters (num: int, inline: bool, etc.)
|
|
43
|
+
:param sync: Run synchronously (blocking) if True
|
|
44
|
+
:return: True if started
|
|
45
|
+
"""
|
|
46
|
+
extra = extra or {}
|
|
47
|
+
ctx = context.ctx or CtxItem()
|
|
48
|
+
model = context.model
|
|
49
|
+
prompt = context.prompt
|
|
50
|
+
num = int(extra.get("num", 1))
|
|
51
|
+
inline = bool(extra.get("inline", False))
|
|
52
|
+
|
|
53
|
+
# Optional prompt enhancement model (same as in your Google path)
|
|
54
|
+
prompt_model = self.window.core.models.from_defaults()
|
|
55
|
+
tmp = self.window.core.config.get('img_prompt_model')
|
|
56
|
+
if self.window.core.models.has(tmp):
|
|
57
|
+
prompt_model = self.window.core.models.get(tmp)
|
|
58
|
+
|
|
59
|
+
worker = ImageWorker()
|
|
60
|
+
worker.window = self.window
|
|
61
|
+
worker.ctx = ctx
|
|
62
|
+
worker.model = model.id or "grok-2-image"
|
|
63
|
+
worker.input_prompt = prompt
|
|
64
|
+
worker.model_prompt = prompt_model
|
|
65
|
+
worker.system_prompt = self.window.core.prompt.get('img')
|
|
66
|
+
worker.raw = self.window.core.config.get('img_raw')
|
|
67
|
+
worker.num = num
|
|
68
|
+
worker.inline = inline
|
|
69
|
+
|
|
70
|
+
self.worker = worker
|
|
71
|
+
self.worker.signals.finished.connect(self.window.core.image.handle_finished)
|
|
72
|
+
self.worker.signals.finished_inline.connect(self.window.core.image.handle_finished_inline)
|
|
73
|
+
self.worker.signals.status.connect(self.window.core.image.handle_status)
|
|
74
|
+
self.worker.signals.error.connect(self.window.core.image.handle_error)
|
|
75
|
+
|
|
76
|
+
if sync or not self.window.controller.kernel.async_allowed(ctx):
|
|
77
|
+
self.worker.run()
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
self.window.dispatch(KernelEvent(KernelEvent.STATE_BUSY, {"id": "img"}))
|
|
81
|
+
self.window.threadpool.start(self.worker)
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ImageSignals(QObject):
|
|
86
|
+
finished = Signal(object, list, str) # ctx, paths, prompt
|
|
87
|
+
finished_inline = Signal(object, list, str) # ctx, paths, prompt
|
|
88
|
+
status = Signal(object) # message
|
|
89
|
+
error = Signal(object) # exception
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ImageWorker(QRunnable):
|
|
93
|
+
def __init__(self, *args, **kwargs):
|
|
94
|
+
super().__init__()
|
|
95
|
+
self.signals = ImageSignals()
|
|
96
|
+
self.window = None
|
|
97
|
+
self.ctx: Optional[CtxItem] = None
|
|
98
|
+
|
|
99
|
+
# params
|
|
100
|
+
self.model = "grok-2-image"
|
|
101
|
+
self.model_prompt = None
|
|
102
|
+
self.input_prompt = ""
|
|
103
|
+
self.system_prompt = ""
|
|
104
|
+
self.inline = False
|
|
105
|
+
self.raw = False
|
|
106
|
+
self.num = 1
|
|
107
|
+
|
|
108
|
+
# API
|
|
109
|
+
self.api_url = "https://api.x.ai/v1/images/generations" # OpenAI-compatible endpoint
|
|
110
|
+
|
|
111
|
+
@Slot()
|
|
112
|
+
def run(self):
|
|
113
|
+
try:
|
|
114
|
+
# optional prompt enhancement
|
|
115
|
+
if not self.raw and not self.inline and self.input_prompt:
|
|
116
|
+
try:
|
|
117
|
+
self.signals.status.emit(trans('img.status.prompt.wait'))
|
|
118
|
+
bridge_context = BridgeContext(
|
|
119
|
+
prompt=self.input_prompt,
|
|
120
|
+
system_prompt=self.system_prompt,
|
|
121
|
+
model=self.model_prompt,
|
|
122
|
+
max_tokens=200,
|
|
123
|
+
temperature=1.0,
|
|
124
|
+
)
|
|
125
|
+
ev = KernelEvent(KernelEvent.CALL, {'context': bridge_context, 'extra': {}})
|
|
126
|
+
self.window.dispatch(ev)
|
|
127
|
+
resp = ev.data.get('response')
|
|
128
|
+
if resp:
|
|
129
|
+
self.input_prompt = resp
|
|
130
|
+
except Exception as e:
|
|
131
|
+
self.signals.error.emit(e)
|
|
132
|
+
self.signals.status.emit(trans('img.status.prompt.error') + ": " + str(e))
|
|
133
|
+
|
|
134
|
+
self.signals.status.emit(trans('img.status.generating') + f": {self.input_prompt}...")
|
|
135
|
+
|
|
136
|
+
cfg = self.window.core.config
|
|
137
|
+
api_key = cfg.get("api_key_xai") or os.environ.get("XAI_API_KEY") or ""
|
|
138
|
+
self.api_url = cfg.get("api_endpoint_xai") + "/images/generations"
|
|
139
|
+
if not api_key:
|
|
140
|
+
raise RuntimeError("Missing xAI API key. Set `api_key_xai` in config or XAI_API_KEY in env.")
|
|
141
|
+
|
|
142
|
+
headers = {
|
|
143
|
+
"Authorization": f"Bearer {api_key}",
|
|
144
|
+
"Content-Type": "application/json",
|
|
145
|
+
}
|
|
146
|
+
payload = {
|
|
147
|
+
"model": self.model or "grok-2-image",
|
|
148
|
+
"prompt": self.input_prompt or "",
|
|
149
|
+
"n": max(1, min(int(self.num), 10)),
|
|
150
|
+
"response_format": "b64_json", # get base64 so we can save locally
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
r = requests.post(self.api_url, headers=headers, json=payload, timeout=180)
|
|
154
|
+
r.raise_for_status()
|
|
155
|
+
data = r.json()
|
|
156
|
+
|
|
157
|
+
images = []
|
|
158
|
+
for idx, img in enumerate((data.get("data") or [])[: self.num]):
|
|
159
|
+
b64 = img.get("b64_json")
|
|
160
|
+
if not b64:
|
|
161
|
+
# fallback: url download
|
|
162
|
+
url = img.get("url")
|
|
163
|
+
if url:
|
|
164
|
+
try:
|
|
165
|
+
rr = requests.get(url, timeout=60)
|
|
166
|
+
if rr.status_code == 200:
|
|
167
|
+
images.append(rr.content)
|
|
168
|
+
except Exception:
|
|
169
|
+
pass
|
|
170
|
+
continue
|
|
171
|
+
try:
|
|
172
|
+
images.append(base64.b64decode(b64))
|
|
173
|
+
except Exception:
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
paths: List[str] = []
|
|
177
|
+
for i, content in enumerate(images):
|
|
178
|
+
name = (
|
|
179
|
+
datetime.date.today().strftime("%Y-%m-%d") + "_" +
|
|
180
|
+
datetime.datetime.now().strftime("%H-%M-%S") + "-" +
|
|
181
|
+
self.window.core.image.make_safe_filename(self.input_prompt) + "-" +
|
|
182
|
+
str(i + 1) + ".jpg"
|
|
183
|
+
)
|
|
184
|
+
path = os.path.join(self.window.core.config.get_user_dir("img"), name)
|
|
185
|
+
self.signals.status.emit(trans('img.status.downloading') + f" ({i + 1} / {self.num}) -> {path}")
|
|
186
|
+
|
|
187
|
+
if self.window.core.image.save_image(path, content):
|
|
188
|
+
paths.append(path)
|
|
189
|
+
|
|
190
|
+
if self.inline:
|
|
191
|
+
self.signals.finished_inline.emit(self.ctx, paths, self.input_prompt)
|
|
192
|
+
else:
|
|
193
|
+
self.signals.finished.emit(self.ctx, paths, self.input_prompt)
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
self.signals.error.emit(e)
|
|
197
|
+
finally:
|
|
198
|
+
self._cleanup()
|
|
199
|
+
|
|
200
|
+
def _cleanup(self):
|
|
201
|
+
"""Cleanup signals to avoid multiple calls."""
|
|
202
|
+
sig = self.signals
|
|
203
|
+
self.signals = None
|
|
204
|
+
if sig is not None:
|
|
205
|
+
try:
|
|
206
|
+
sig.deleteLater()
|
|
207
|
+
except RuntimeError:
|
|
208
|
+
pass
|
|
@@ -0,0 +1,262 @@
|
|
|
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: 2025.09.05 01:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
from typing import Optional, Dict, Any, List
|
|
14
|
+
|
|
15
|
+
from pygpt_net.item.model import ModelItem
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Remote:
|
|
19
|
+
def __init__(self, window=None):
|
|
20
|
+
"""
|
|
21
|
+
Live Search builder for xAI:
|
|
22
|
+
- Returns a dict with 'sdk' (X-only toggle) and 'http' (full search_parameters).
|
|
23
|
+
- SDK path: native streaming (xai_sdk.chat.stream()) works only for basic X search (no advanced filters).
|
|
24
|
+
- HTTP path: full Live Search (web/news/rss/X with filters), streaming via SSE.
|
|
25
|
+
|
|
26
|
+
:param window: Window instance
|
|
27
|
+
"""
|
|
28
|
+
self.window = window
|
|
29
|
+
|
|
30
|
+
def build_remote_tools(self, model: ModelItem = None) -> Dict[str, Any]:
|
|
31
|
+
"""
|
|
32
|
+
Return live-search config for xAI:
|
|
33
|
+
{
|
|
34
|
+
"mode": ...,
|
|
35
|
+
"sdk": {"enabled": bool},
|
|
36
|
+
"http": Optional[dict],
|
|
37
|
+
"reason": Optional[str],
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
:param model: ModelItem (optional, not used now)
|
|
41
|
+
:return: Dict with 'sdk' and 'http' keys
|
|
42
|
+
"""
|
|
43
|
+
return self.build(model)
|
|
44
|
+
|
|
45
|
+
def build(self, model=None) -> Dict[str, Any]:
|
|
46
|
+
"""
|
|
47
|
+
Build both SDK-capable toggle and HTTP search_parameters.
|
|
48
|
+
Returns:
|
|
49
|
+
{
|
|
50
|
+
"mode": "auto"|"on"|"off",
|
|
51
|
+
"sdk": {"enabled": bool}, # True if we can use native SDK stream (X-only no filters)
|
|
52
|
+
"http": Optional[dict], # search_parameters for Chat Completions (or None)
|
|
53
|
+
"reason": Optional[str], # diagnostic: why http is needed
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
:param model: ModelItem (not used now)
|
|
57
|
+
:return: Dict with 'sdk' and 'http' keys
|
|
58
|
+
"""
|
|
59
|
+
cfg = self.window.core.config
|
|
60
|
+
|
|
61
|
+
mode = str(cfg.get("remote_tools.xai.mode") or "auto").lower()
|
|
62
|
+
if mode not in ("auto", "on", "off"):
|
|
63
|
+
mode = "auto"
|
|
64
|
+
|
|
65
|
+
# sources toggles
|
|
66
|
+
s_web = bool(cfg.get("remote_tools.xai.sources.web", True))
|
|
67
|
+
s_x = bool(cfg.get("remote_tools.xai.sources.x", True))
|
|
68
|
+
s_news = bool(cfg.get("remote_tools.xai.sources.news", False))
|
|
69
|
+
s_rss = bool(cfg.get("remote_tools.xai.sources.rss", False))
|
|
70
|
+
|
|
71
|
+
# advanced flags
|
|
72
|
+
adv_web_allowed = self._has_list(cfg.get("remote_tools.xai.web.allowed_websites"))
|
|
73
|
+
adv_web_excluded = self._has_list(cfg.get("remote_tools.xai.web.excluded_websites"))
|
|
74
|
+
adv_web_country = self._has_str(cfg.get("remote_tools.xai.web.country"))
|
|
75
|
+
adv_web_safe = cfg.get("remote_tools.xai.web.safe_search", None)
|
|
76
|
+
|
|
77
|
+
adv_news_excl = self._has_list(cfg.get("remote_tools.xai.news.excluded_websites"))
|
|
78
|
+
adv_news_country = self._has_str(cfg.get("remote_tools.xai.news.country"))
|
|
79
|
+
adv_news_safe = cfg.get("remote_tools.xai.news.safe_search", None)
|
|
80
|
+
|
|
81
|
+
adv_x_incl = self._has_list(cfg.get("remote_tools.xai.x.included_handles"))
|
|
82
|
+
adv_x_excl = self._has_list(cfg.get("remote_tools.xai.x.excluded_handles"))
|
|
83
|
+
adv_x_favs = self._has_int(cfg.get("remote_tools.xai.x.min_favs"))
|
|
84
|
+
adv_x_views = self._has_int(cfg.get("remote_tools.xai.x.min_views"))
|
|
85
|
+
|
|
86
|
+
adv_rss_link = self._has_str(cfg.get("remote_tools.xai.rss.link"))
|
|
87
|
+
|
|
88
|
+
adv_from = self._has_str(cfg.get("remote_tools.xai.from_date"))
|
|
89
|
+
adv_to = self._has_str(cfg.get("remote_tools.xai.to_date"))
|
|
90
|
+
|
|
91
|
+
adv_max_results = self._has_int(cfg.get("remote_tools.xai.max_results"))
|
|
92
|
+
adv_return_cits = cfg.get("remote_tools.xai.return_citations", True) is not True # different than default?
|
|
93
|
+
|
|
94
|
+
# SDK-capable if: mode!=off and ONLY X is enabled and no X filters/date/max_results customizations
|
|
95
|
+
x_only = s_x and not s_web and not s_news and not s_rss
|
|
96
|
+
x_filters = any([adv_x_incl, adv_x_excl, adv_x_favs, adv_x_views])
|
|
97
|
+
sdk_enabled = (mode != "off") and x_only and not any([
|
|
98
|
+
x_filters, adv_from, adv_to, adv_max_results, adv_return_cits
|
|
99
|
+
])
|
|
100
|
+
|
|
101
|
+
# Build HTTP search_parameters only when needed beyond X-only basic,
|
|
102
|
+
# or when mode explicitly on/auto but SDK cannot represent flags.
|
|
103
|
+
http_params: Optional[dict] = None
|
|
104
|
+
http_reason: Optional[str] = None
|
|
105
|
+
|
|
106
|
+
need_http = (mode != "off") and (
|
|
107
|
+
not sdk_enabled or # advanced X filters or other sources/date/results/citations
|
|
108
|
+
s_web or s_news or s_rss
|
|
109
|
+
)
|
|
110
|
+
if need_http:
|
|
111
|
+
http_params = self._build_http_params(cfg, mode, s_web, s_x, s_news, s_rss)
|
|
112
|
+
http_reason = "advanced_sources_or_filters"
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
"mode": mode,
|
|
116
|
+
"sdk": {"enabled": sdk_enabled},
|
|
117
|
+
"http": http_params,
|
|
118
|
+
"reason": http_reason,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# ---------- helpers ----------
|
|
122
|
+
|
|
123
|
+
def _build_http_params(
|
|
124
|
+
self,
|
|
125
|
+
cfg,
|
|
126
|
+
mode: str,
|
|
127
|
+
s_web: bool,
|
|
128
|
+
s_x: bool,
|
|
129
|
+
s_news: bool,
|
|
130
|
+
s_rss: bool
|
|
131
|
+
) -> dict:
|
|
132
|
+
"""
|
|
133
|
+
Build search_parameters for Chat Completions (HTTP).
|
|
134
|
+
|
|
135
|
+
:param cfg: Config dict
|
|
136
|
+
:param mode: "auto"|"on"|"off"
|
|
137
|
+
:param s_web: Include web search
|
|
138
|
+
:param s_x: Include X search
|
|
139
|
+
:param s_news: Include news search
|
|
140
|
+
:param s_rss: Include RSS search
|
|
141
|
+
:return: search_parameters dict
|
|
142
|
+
"""
|
|
143
|
+
params: Dict[str, Any] = {"mode": mode}
|
|
144
|
+
|
|
145
|
+
rc = cfg.get("remote_tools.xai.return_citations")
|
|
146
|
+
if rc is None:
|
|
147
|
+
rc = True
|
|
148
|
+
params["return_citations"] = bool(rc)
|
|
149
|
+
|
|
150
|
+
msr = cfg.get("remote_tools.xai.max_results")
|
|
151
|
+
if isinstance(msr, int) and msr > 0:
|
|
152
|
+
params["max_search_results"] = int(msr)
|
|
153
|
+
|
|
154
|
+
for k_cfg, k_out in (("remote_tools.xai.from_date", "from_date"),
|
|
155
|
+
("remote_tools.xai.to_date", "to_date")):
|
|
156
|
+
val = cfg.get(k_cfg)
|
|
157
|
+
if isinstance(val, str) and val.strip():
|
|
158
|
+
params[k_out] = val.strip()
|
|
159
|
+
|
|
160
|
+
sources: List[Dict[str, Any]] = []
|
|
161
|
+
|
|
162
|
+
if s_web:
|
|
163
|
+
web: Dict[str, Any] = {"type": "web"}
|
|
164
|
+
country = cfg.get("remote_tools.xai.web.country")
|
|
165
|
+
if isinstance(country, str) and len(country.strip()) == 2:
|
|
166
|
+
web["country"] = country.strip().upper()
|
|
167
|
+
allowed = self._as_list(cfg.get("remote_tools.xai.web.allowed_websites"), 5)
|
|
168
|
+
excluded = self._as_list(cfg.get("remote_tools.xai.web.excluded_websites"), 5)
|
|
169
|
+
if allowed:
|
|
170
|
+
web["allowed_websites"] = allowed
|
|
171
|
+
elif excluded:
|
|
172
|
+
web["excluded_websites"] = excluded
|
|
173
|
+
safe = cfg.get("remote_tools.xai.web.safe_search")
|
|
174
|
+
if safe is not None:
|
|
175
|
+
web["safe_search"] = bool(safe)
|
|
176
|
+
sources.append(web)
|
|
177
|
+
|
|
178
|
+
if s_x:
|
|
179
|
+
xsrc: Dict[str, Any] = {"type": "x"}
|
|
180
|
+
inc = self._as_list(cfg.get("remote_tools.xai.x.included_handles"), 10)
|
|
181
|
+
exc = self._as_list(cfg.get("remote_tools.xai.x.excluded_handles"), 10)
|
|
182
|
+
if inc and not exc:
|
|
183
|
+
xsrc["included_x_handles"] = inc
|
|
184
|
+
elif exc and not inc:
|
|
185
|
+
xsrc["excluded_x_handles"] = exc
|
|
186
|
+
favs = cfg.get("remote_tools.xai.x.min_favs")
|
|
187
|
+
views = cfg.get("remote_tools.xai.x.min_views")
|
|
188
|
+
if isinstance(favs, int) and favs > 0:
|
|
189
|
+
xsrc["post_favorite_count"] = int(favs)
|
|
190
|
+
if isinstance(views, int) and views > 0:
|
|
191
|
+
xsrc["post_view_count"] = int(views)
|
|
192
|
+
sources.append(xsrc)
|
|
193
|
+
|
|
194
|
+
if s_news:
|
|
195
|
+
news: Dict[str, Any] = {"type": "news"}
|
|
196
|
+
country = cfg.get("remote_tools.xai.news.country")
|
|
197
|
+
if isinstance(country, str) and len(country.strip()) == 2:
|
|
198
|
+
news["country"] = country.strip().upper()
|
|
199
|
+
excluded = self._as_list(cfg.get("remote_tools.xai.news.excluded_websites"), 5)
|
|
200
|
+
if excluded:
|
|
201
|
+
news["excluded_websites"] = excluded
|
|
202
|
+
safe = cfg.get("remote_tools.xai.news.safe_search")
|
|
203
|
+
if safe is not None:
|
|
204
|
+
news["safe_search"] = bool(safe)
|
|
205
|
+
sources.append(news)
|
|
206
|
+
|
|
207
|
+
if s_rss:
|
|
208
|
+
link = cfg.get("remote_tools.xai.rss.link")
|
|
209
|
+
if isinstance(link, str) and link.strip():
|
|
210
|
+
sources.append({"type": "rss", "links": [link.strip()]})
|
|
211
|
+
|
|
212
|
+
if sources:
|
|
213
|
+
params["sources"] = sources
|
|
214
|
+
|
|
215
|
+
return params
|
|
216
|
+
|
|
217
|
+
def _has_list(self, v) -> bool:
|
|
218
|
+
"""
|
|
219
|
+
Return True if v is a non-empty list/tuple or a non-empty comma-separated string.
|
|
220
|
+
|
|
221
|
+
:param v: Any
|
|
222
|
+
:return: bool
|
|
223
|
+
"""
|
|
224
|
+
if v is None:
|
|
225
|
+
return False
|
|
226
|
+
if isinstance(v, (list, tuple)):
|
|
227
|
+
return len([x for x in v if str(x).strip()]) > 0
|
|
228
|
+
return len([x for x in str(v).split(",") if x.strip()]) > 0
|
|
229
|
+
|
|
230
|
+
def _has_str(self, v) -> bool:
|
|
231
|
+
"""
|
|
232
|
+
Return True if v is a non-empty string.
|
|
233
|
+
|
|
234
|
+
:param v: Any
|
|
235
|
+
:return: bool
|
|
236
|
+
"""
|
|
237
|
+
return isinstance(v, str) and bool(v.strip())
|
|
238
|
+
|
|
239
|
+
def _has_int(self, v) -> bool:
|
|
240
|
+
"""
|
|
241
|
+
Return True if v is a positive integer.
|
|
242
|
+
|
|
243
|
+
:param v: Any
|
|
244
|
+
:return: true if v is a positive integer
|
|
245
|
+
"""
|
|
246
|
+
return isinstance(v, int) and v > 0
|
|
247
|
+
|
|
248
|
+
def _as_list(self, v, limit: int) -> List[str]:
|
|
249
|
+
"""
|
|
250
|
+
Convert v to a list of strings, limited to 'limit' items.
|
|
251
|
+
|
|
252
|
+
:param v: Any
|
|
253
|
+
:param limit: limit number of items
|
|
254
|
+
:return: List of strings
|
|
255
|
+
"""
|
|
256
|
+
if v is None:
|
|
257
|
+
return []
|
|
258
|
+
if isinstance(v, (list, tuple)):
|
|
259
|
+
arr = [str(x).strip() for x in v if str(x).strip()]
|
|
260
|
+
else:
|
|
261
|
+
arr = [s.strip() for s in str(v).split(",") if s.strip()]
|
|
262
|
+
return arr[:limit]
|
|
@@ -0,0 +1,120 @@
|
|
|
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: 2025.09.05 01:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from typing import List, Any, Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Tools:
|
|
17
|
+
def __init__(self, window=None):
|
|
18
|
+
"""
|
|
19
|
+
Tools mapper for xAI Chat Completions-compatible schema.
|
|
20
|
+
|
|
21
|
+
Input: app 'functions' list with keys: name, desc, params (JSON Schema string).
|
|
22
|
+
Output: list of dicts with keys: name, description, parameters.
|
|
23
|
+
|
|
24
|
+
:param window: Window instance
|
|
25
|
+
"""
|
|
26
|
+
self.window = window
|
|
27
|
+
|
|
28
|
+
def _sanitize_schema(self, schema: Any) -> Any:
|
|
29
|
+
"""
|
|
30
|
+
Sanitize JSON Schema for tool parameters:
|
|
31
|
+
- Remove unsupported or risky keywords.
|
|
32
|
+
- Normalize 'type'.
|
|
33
|
+
- Ensure properties/items recursively valid.
|
|
34
|
+
|
|
35
|
+
:param schema: JSON Schema (dict or list)
|
|
36
|
+
:return: Sanitized JSON Schema (dict)
|
|
37
|
+
"""
|
|
38
|
+
if isinstance(schema, list):
|
|
39
|
+
return self._sanitize_schema(schema[0]) if schema else {}
|
|
40
|
+
|
|
41
|
+
if not isinstance(schema, dict):
|
|
42
|
+
return schema
|
|
43
|
+
|
|
44
|
+
banned = {
|
|
45
|
+
"$defs", "$ref", "$schema", "$id",
|
|
46
|
+
"oneOf", "anyOf", "allOf",
|
|
47
|
+
"patternProperties", "dependentSchemas", "dependentRequired",
|
|
48
|
+
"unevaluatedProperties", "nullable", "readOnly", "writeOnly",
|
|
49
|
+
"examples", "additional_properties", "additionalProperties",
|
|
50
|
+
}
|
|
51
|
+
for k in list(schema.keys()):
|
|
52
|
+
if k in banned:
|
|
53
|
+
schema.pop(k, None)
|
|
54
|
+
|
|
55
|
+
t = schema.get("type")
|
|
56
|
+
if isinstance(t, list):
|
|
57
|
+
t_no_null = [x for x in t if x != "null"]
|
|
58
|
+
schema["type"] = t_no_null[0] if t_no_null else "object"
|
|
59
|
+
|
|
60
|
+
# Recurse
|
|
61
|
+
if (schema.get("type") or "").lower() == "object":
|
|
62
|
+
props = schema.get("properties")
|
|
63
|
+
if not isinstance(props, dict):
|
|
64
|
+
props = {}
|
|
65
|
+
clean = {}
|
|
66
|
+
for k, v in props.items():
|
|
67
|
+
clean[k] = self._sanitize_schema(v)
|
|
68
|
+
schema["properties"] = clean
|
|
69
|
+
req = schema.get("required")
|
|
70
|
+
if not isinstance(req, list) or not all(isinstance(x, str) for x in req):
|
|
71
|
+
schema.pop("required", None)
|
|
72
|
+
elif len(req) == 0:
|
|
73
|
+
schema.pop("required", None)
|
|
74
|
+
|
|
75
|
+
if (schema.get("type") or "").lower() == "array":
|
|
76
|
+
items = schema.get("items")
|
|
77
|
+
if isinstance(items, list) and items:
|
|
78
|
+
items = items[0]
|
|
79
|
+
if not isinstance(items, dict):
|
|
80
|
+
items = {"type": "string"}
|
|
81
|
+
schema["items"] = self._sanitize_schema(items)
|
|
82
|
+
|
|
83
|
+
return schema
|
|
84
|
+
|
|
85
|
+
def prepare(self, functions: list) -> List[dict]:
|
|
86
|
+
"""
|
|
87
|
+
Prepare xAI tools list (OpenAI-compatible schema) from app functions list.
|
|
88
|
+
|
|
89
|
+
Returns [] if no functions provided.
|
|
90
|
+
|
|
91
|
+
: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)
|
|
93
|
+
"""
|
|
94
|
+
if not functions or not isinstance(functions, list):
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
tools: List[dict] = []
|
|
98
|
+
for fn in functions:
|
|
99
|
+
name = str(fn.get("name") or "").strip()
|
|
100
|
+
if not name:
|
|
101
|
+
continue
|
|
102
|
+
desc = fn.get("desc") or ""
|
|
103
|
+
params: Optional[dict] = {}
|
|
104
|
+
if fn.get("params"):
|
|
105
|
+
try:
|
|
106
|
+
params = json.loads(fn["params"])
|
|
107
|
+
except Exception:
|
|
108
|
+
params = {}
|
|
109
|
+
params = self._sanitize_schema(params or {})
|
|
110
|
+
if not params.get("type"):
|
|
111
|
+
params["type"] = "object"
|
|
112
|
+
else:
|
|
113
|
+
params = {"type": "object"}
|
|
114
|
+
|
|
115
|
+
tools.append({
|
|
116
|
+
"name": name,
|
|
117
|
+
"description": desc,
|
|
118
|
+
"parameters": params,
|
|
119
|
+
})
|
|
120
|
+
return tools
|