pygpt-net 2.6.52__py3-none-any.whl → 2.6.54__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 +11 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +4 -0
- pygpt_net/controller/audio/audio.py +22 -1
- pygpt_net/controller/chat/chat.py +5 -1
- pygpt_net/controller/chat/remote_tools.py +116 -0
- pygpt_net/controller/lang/mapping.py +2 -1
- pygpt_net/controller/mode/mode.py +5 -2
- pygpt_net/controller/plugins/plugins.py +29 -3
- pygpt_net/controller/realtime/realtime.py +8 -3
- pygpt_net/controller/ui/mode.py +17 -5
- pygpt_net/core/agents/provider.py +16 -9
- pygpt_net/core/models/models.py +25 -1
- pygpt_net/core/render/web/renderer.py +3 -1
- pygpt_net/data/config/config.json +5 -4
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/icons/web_off.svg +1 -0
- pygpt_net/data/icons/web_on.svg +1 -0
- pygpt_net/data/js/app.js +19 -0
- pygpt_net/data/locale/locale.de.ini +1 -0
- pygpt_net/data/locale/locale.en.ini +3 -2
- pygpt_net/data/locale/locale.es.ini +1 -0
- pygpt_net/data/locale/locale.fr.ini +1 -0
- pygpt_net/data/locale/locale.it.ini +1 -0
- pygpt_net/data/locale/locale.pl.ini +1 -4
- pygpt_net/data/locale/locale.uk.ini +1 -0
- pygpt_net/data/locale/locale.zh.ini +1 -0
- pygpt_net/data/locale/plugin.mcp.en.ini +4 -4
- pygpt_net/data/locale/plugin.osm.en.ini +35 -0
- pygpt_net/data/locale/plugin.wolfram.en.ini +24 -0
- pygpt_net/icons.qrc +2 -0
- pygpt_net/icons_rc.py +232 -147
- pygpt_net/js_rc.py +10490 -10432
- pygpt_net/plugin/base/worker.py +7 -1
- pygpt_net/plugin/osm/__init__.py +12 -0
- pygpt_net/plugin/osm/config.py +267 -0
- pygpt_net/plugin/osm/plugin.py +87 -0
- pygpt_net/plugin/osm/worker.py +719 -0
- pygpt_net/plugin/wolfram/__init__.py +12 -0
- pygpt_net/plugin/wolfram/config.py +214 -0
- pygpt_net/plugin/wolfram/plugin.py +115 -0
- pygpt_net/plugin/wolfram/worker.py +551 -0
- pygpt_net/provider/api/anthropic/tools.py +4 -2
- pygpt_net/provider/api/google/__init__.py +3 -2
- pygpt_net/provider/api/google/video.py +0 -0
- pygpt_net/provider/api/openai/agents/experts.py +1 -1
- pygpt_net/provider/api/openai/agents/remote_tools.py +14 -4
- pygpt_net/provider/api/openai/chat.py +7 -2
- pygpt_net/provider/api/openai/remote_tools.py +5 -2
- pygpt_net/provider/api/x_ai/remote.py +6 -1
- pygpt_net/provider/core/config/patch.py +8 -1
- pygpt_net/provider/llms/anthropic.py +29 -1
- pygpt_net/provider/llms/google.py +30 -1
- pygpt_net/provider/llms/open_router.py +3 -1
- pygpt_net/provider/llms/x_ai.py +21 -1
- pygpt_net/ui/layout/chat/output.py +7 -2
- {pygpt_net-2.6.52.dist-info → pygpt_net-2.6.54.dist-info}/METADATA +37 -2
- {pygpt_net-2.6.52.dist-info → pygpt_net-2.6.54.dist-info}/RECORD +60 -47
- {pygpt_net-2.6.52.dist-info → pygpt_net-2.6.54.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.52.dist-info → pygpt_net-2.6.54.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.52.dist-info → pygpt_net-2.6.54.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,551 @@
|
|
|
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.18 00:25:36 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import time
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
from urllib.parse import quote # kept for parity, not strictly needed everywhere
|
|
20
|
+
|
|
21
|
+
import requests
|
|
22
|
+
from PySide6.QtCore import Slot
|
|
23
|
+
|
|
24
|
+
from pygpt_net.plugin.base.worker import BaseWorker, BaseSignals
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class WorkerSignals(BaseSignals):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Worker(BaseWorker):
|
|
32
|
+
"""
|
|
33
|
+
Wolfram Alpha plugin worker: Short answers, Full JSON query, Math compute,
|
|
34
|
+
Solve equations, Derivatives, Integrals, Unit conversions, Matrix ops, Plots.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, *args, **kwargs):
|
|
38
|
+
super(Worker, self).__init__()
|
|
39
|
+
self.signals = WorkerSignals()
|
|
40
|
+
self.args = args
|
|
41
|
+
self.kwargs = kwargs
|
|
42
|
+
self.plugin = None
|
|
43
|
+
self.cmds = None
|
|
44
|
+
self.ctx = None
|
|
45
|
+
self.msg = None
|
|
46
|
+
|
|
47
|
+
# ---------------------- Core runner ----------------------
|
|
48
|
+
|
|
49
|
+
@Slot()
|
|
50
|
+
def run(self):
|
|
51
|
+
try:
|
|
52
|
+
responses = []
|
|
53
|
+
for item in self.cmds:
|
|
54
|
+
if self.is_stopped():
|
|
55
|
+
break
|
|
56
|
+
try:
|
|
57
|
+
response = None
|
|
58
|
+
if item["cmd"] in self.plugin.allowed_cmds and self.plugin.has_cmd(item["cmd"]):
|
|
59
|
+
|
|
60
|
+
# Generic query endpoints
|
|
61
|
+
if item["cmd"] == "wa_short":
|
|
62
|
+
response = self.cmd_wa_short(item)
|
|
63
|
+
elif item["cmd"] == "wa_spoken":
|
|
64
|
+
response = self.cmd_wa_spoken(item)
|
|
65
|
+
elif item["cmd"] == "wa_simple":
|
|
66
|
+
response = self.cmd_wa_simple(item)
|
|
67
|
+
elif item["cmd"] == "wa_query":
|
|
68
|
+
response = self.cmd_wa_query(item)
|
|
69
|
+
|
|
70
|
+
# Convenience math commands
|
|
71
|
+
elif item["cmd"] == "wa_calculate":
|
|
72
|
+
response = self.cmd_wa_calculate(item)
|
|
73
|
+
elif item["cmd"] == "wa_solve":
|
|
74
|
+
response = self.cmd_wa_solve(item)
|
|
75
|
+
elif item["cmd"] == "wa_derivative":
|
|
76
|
+
response = self.cmd_wa_derivative(item)
|
|
77
|
+
elif item["cmd"] == "wa_integral":
|
|
78
|
+
response = self.cmd_wa_integral(item)
|
|
79
|
+
elif item["cmd"] == "wa_units_convert":
|
|
80
|
+
response = self.cmd_wa_units_convert(item)
|
|
81
|
+
elif item["cmd"] == "wa_matrix":
|
|
82
|
+
response = self.cmd_wa_matrix(item)
|
|
83
|
+
elif item["cmd"] == "wa_plot":
|
|
84
|
+
response = self.cmd_wa_plot(item)
|
|
85
|
+
|
|
86
|
+
if response is not None:
|
|
87
|
+
responses.append(response)
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
responses.append(self.make_response(item, self.throw_error(e)))
|
|
91
|
+
|
|
92
|
+
if responses:
|
|
93
|
+
self.reply_more(responses)
|
|
94
|
+
if self.msg is not None:
|
|
95
|
+
self.status(self.msg)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
self.error(e)
|
|
98
|
+
finally:
|
|
99
|
+
self.cleanup()
|
|
100
|
+
|
|
101
|
+
# ---------------------- HTTP / Helpers ----------------------
|
|
102
|
+
|
|
103
|
+
def _api_base(self) -> str:
|
|
104
|
+
return (self.plugin.get_option_value("api_base") or "https://api.wolframalpha.com").rstrip("/")
|
|
105
|
+
|
|
106
|
+
def _timeout(self) -> int:
|
|
107
|
+
try:
|
|
108
|
+
return int(self.plugin.get_option_value("http_timeout") or 30)
|
|
109
|
+
except Exception:
|
|
110
|
+
return 30
|
|
111
|
+
|
|
112
|
+
def _appid(self) -> str:
|
|
113
|
+
appid = (self.plugin.get_option_value("wa_appid") or "").strip()
|
|
114
|
+
if not appid:
|
|
115
|
+
raise RuntimeError("Missing Wolfram Alpha AppID (set 'wa_appid' in plugin options).")
|
|
116
|
+
return appid
|
|
117
|
+
|
|
118
|
+
def _units(self) -> Optional[str]:
|
|
119
|
+
units = (self.plugin.get_option_value("units") or "").strip().lower()
|
|
120
|
+
return units if units in ("metric", "nonmetric") else None
|
|
121
|
+
|
|
122
|
+
def _headers(self) -> Dict[str, str]:
|
|
123
|
+
return {
|
|
124
|
+
"User-Agent": "pygpt-net-wolframalpha-plugin/1.0",
|
|
125
|
+
"Accept": "*/*",
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
def _get_raw(self, path: str, params: dict | None = None) -> requests.Response:
|
|
129
|
+
url = f"{self._api_base()}{path}"
|
|
130
|
+
r = requests.get(url, headers=self._headers(), params=params or {}, timeout=self._timeout())
|
|
131
|
+
return r
|
|
132
|
+
|
|
133
|
+
def _handle_json(self, r: requests.Response) -> dict:
|
|
134
|
+
# For /v2/query?output=json
|
|
135
|
+
try:
|
|
136
|
+
payload = r.json() if r.content else None
|
|
137
|
+
except Exception:
|
|
138
|
+
payload = r.text
|
|
139
|
+
|
|
140
|
+
if not (200 <= r.status_code < 300):
|
|
141
|
+
message = payload if isinstance(payload, str) else None
|
|
142
|
+
raise RuntimeError(json.dumps({
|
|
143
|
+
"status": r.status_code,
|
|
144
|
+
"error": message or "HTTP error",
|
|
145
|
+
}, ensure_ascii=False))
|
|
146
|
+
|
|
147
|
+
# Normalize
|
|
148
|
+
if isinstance(payload, dict):
|
|
149
|
+
ret = payload
|
|
150
|
+
else:
|
|
151
|
+
ret = {"data": payload}
|
|
152
|
+
|
|
153
|
+
ret["_meta"] = {
|
|
154
|
+
"status": r.status_code,
|
|
155
|
+
"content_type": r.headers.get("Content-Type"),
|
|
156
|
+
}
|
|
157
|
+
return ret
|
|
158
|
+
|
|
159
|
+
def _handle_text(self, r: requests.Response) -> dict:
|
|
160
|
+
# For short/spoken result endpoints
|
|
161
|
+
txt = r.text if r.content else ""
|
|
162
|
+
if not (200 <= r.status_code < 300):
|
|
163
|
+
# WA short endpoints may return 501 if no concise answer
|
|
164
|
+
return {
|
|
165
|
+
"ok": False,
|
|
166
|
+
"status": r.status_code,
|
|
167
|
+
"text": txt,
|
|
168
|
+
"_meta": {"content_type": r.headers.get("Content-Type")},
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
"ok": True,
|
|
172
|
+
"status": r.status_code,
|
|
173
|
+
"text": txt,
|
|
174
|
+
"_meta": {"content_type": r.headers.get("Content-Type")},
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
def _save_bytes(self, data: bytes, out_path: str) -> str:
|
|
178
|
+
local = self.prepare_path(out_path)
|
|
179
|
+
os.makedirs(os.path.dirname(local), exist_ok=True)
|
|
180
|
+
with open(local, "wb") as fh:
|
|
181
|
+
fh.write(data)
|
|
182
|
+
return local
|
|
183
|
+
|
|
184
|
+
def _slug(self, s: str, maxlen: int = 80) -> str:
|
|
185
|
+
s = re.sub(r"\s+", "_", s.strip())
|
|
186
|
+
s = re.sub(r"[^a-zA-Z0-9_\-\.]", "", s)
|
|
187
|
+
return (s[:maxlen] or "plot").strip("._-") or "plot"
|
|
188
|
+
|
|
189
|
+
def _now(self) -> int:
|
|
190
|
+
return int(time.time())
|
|
191
|
+
|
|
192
|
+
def prepare_path(self, path: str) -> str:
|
|
193
|
+
if path in [".", "./"]:
|
|
194
|
+
return self.plugin.window.core.config.get_user_dir("data")
|
|
195
|
+
if self.is_absolute_path(path):
|
|
196
|
+
return path
|
|
197
|
+
return os.path.join(self.plugin.window.core.config.get_user_dir("data"), path)
|
|
198
|
+
|
|
199
|
+
def is_absolute_path(self, path: str) -> bool:
|
|
200
|
+
return os.path.isabs(path)
|
|
201
|
+
|
|
202
|
+
# ---------------------- Parsing helpers ----------------------
|
|
203
|
+
|
|
204
|
+
def _extract_primary_plaintext(self, query_json: dict) -> Optional[str]:
|
|
205
|
+
try:
|
|
206
|
+
qr = query_json.get("queryresult") or {}
|
|
207
|
+
pods = qr.get("pods") or []
|
|
208
|
+
# Prefer primary pod
|
|
209
|
+
for p in pods:
|
|
210
|
+
if p.get("primary"):
|
|
211
|
+
texts = [sp.get("plaintext") for sp in (p.get("subpods") or []) if sp.get("plaintext")]
|
|
212
|
+
if texts:
|
|
213
|
+
return "\n".join(texts)
|
|
214
|
+
# Fallback to common pod titles
|
|
215
|
+
prefer_ids = {"Result", "Results", "ExactResult", "Exact result", "Decimal approximation", "Solution", "Solutions"}
|
|
216
|
+
for p in pods:
|
|
217
|
+
if (p.get("id") in prefer_ids) or (p.get("title") in prefer_ids):
|
|
218
|
+
texts = [sp.get("plaintext") for sp in (p.get("subpods") or []) if sp.get("plaintext")]
|
|
219
|
+
if texts:
|
|
220
|
+
return "\n".join(texts)
|
|
221
|
+
# Last resort: first plaintext
|
|
222
|
+
for p in pods:
|
|
223
|
+
for sp in (p.get("subpods") or []):
|
|
224
|
+
if sp.get("plaintext"):
|
|
225
|
+
return sp["plaintext"]
|
|
226
|
+
except Exception:
|
|
227
|
+
pass
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
def _matrix_literal(self, matrix: List[List[Any]]) -> str:
|
|
231
|
+
rows = []
|
|
232
|
+
for row in matrix:
|
|
233
|
+
row_str = ",".join(str(x) for x in row)
|
|
234
|
+
rows.append("{" + row_str + "}")
|
|
235
|
+
return "{" + ",".join(rows) + "}"
|
|
236
|
+
|
|
237
|
+
# ---------------------- Internal cross-call helper ----------------------
|
|
238
|
+
|
|
239
|
+
def _call_cmd(self, cmd: str, params: dict) -> dict:
|
|
240
|
+
"""Call another command inside this worker with a proper envelope so make_response() works."""
|
|
241
|
+
fn = getattr(self, f"cmd_{cmd}", None)
|
|
242
|
+
if not callable(fn):
|
|
243
|
+
raise RuntimeError(f"Unknown command: {cmd}")
|
|
244
|
+
return fn({"cmd": cmd, "params": params})
|
|
245
|
+
|
|
246
|
+
def _add_image(self, path: str):
|
|
247
|
+
"""Register saved image path in context for downstream UI/use."""
|
|
248
|
+
try:
|
|
249
|
+
if self.ctx is None:
|
|
250
|
+
return
|
|
251
|
+
if path:
|
|
252
|
+
path = self.plugin.window.core.filesystem.to_workdir(path)
|
|
253
|
+
# Ensure list exists
|
|
254
|
+
if not hasattr(self.ctx, "images_before") or self.ctx.images_before is None:
|
|
255
|
+
self.ctx.images_before = []
|
|
256
|
+
# Avoid duplicates
|
|
257
|
+
if path and path not in self.ctx.images_before:
|
|
258
|
+
self.ctx.images_before.append(path)
|
|
259
|
+
except Exception:
|
|
260
|
+
# Keep silent: context may not be attached in some runs/tests
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
# ---------------------- Endpoint commands ----------------------
|
|
264
|
+
|
|
265
|
+
def cmd_wa_short(self, item: dict) -> dict:
|
|
266
|
+
p = item.get("params", {})
|
|
267
|
+
query = p.get("query") or p.get("i")
|
|
268
|
+
if not query:
|
|
269
|
+
return self.make_response(item, "Param 'query' required")
|
|
270
|
+
params = {"appid": self._appid(), "i": query}
|
|
271
|
+
units = self._units()
|
|
272
|
+
if units:
|
|
273
|
+
params["units"] = units
|
|
274
|
+
r = self._get_raw("/v2/result", params=params)
|
|
275
|
+
res = self._handle_text(r)
|
|
276
|
+
return self.make_response(item, res)
|
|
277
|
+
|
|
278
|
+
def cmd_wa_spoken(self, item: dict) -> dict:
|
|
279
|
+
p = item.get("params", {})
|
|
280
|
+
query = p.get("query") or p.get("i")
|
|
281
|
+
if not query:
|
|
282
|
+
return self.make_response(item, "Param 'query' required")
|
|
283
|
+
params = {"appid": self._appid(), "i": query}
|
|
284
|
+
units = self._units()
|
|
285
|
+
if units:
|
|
286
|
+
params["units"] = units
|
|
287
|
+
r = self._get_raw("/v1/spoken", params=params)
|
|
288
|
+
res = self._handle_text(r)
|
|
289
|
+
return self.make_response(item, res)
|
|
290
|
+
|
|
291
|
+
def cmd_wa_simple(self, item: dict) -> dict:
|
|
292
|
+
p = item.get("params", {})
|
|
293
|
+
query = p.get("query") or p.get("i")
|
|
294
|
+
out = p.get("out") # suggested file path relative to data dir
|
|
295
|
+
if not query:
|
|
296
|
+
return self.make_response(item, "Param 'query' required")
|
|
297
|
+
params = {"appid": self._appid(), "i": query}
|
|
298
|
+
units = self._units()
|
|
299
|
+
if units:
|
|
300
|
+
params["units"] = units
|
|
301
|
+
# Optional presentation params
|
|
302
|
+
bg = (p.get("background") or self.plugin.get_option_value("simple_background") or "white").lower()
|
|
303
|
+
if bg in ("white", "transparent"):
|
|
304
|
+
params["background"] = bg
|
|
305
|
+
layout = (p.get("layout") or self.plugin.get_option_value("simple_layout") or "labelbar").lower()
|
|
306
|
+
params["layout"] = layout
|
|
307
|
+
width = p.get("width") or self.plugin.get_option_value("simple_width")
|
|
308
|
+
if width:
|
|
309
|
+
params["width"] = int(width)
|
|
310
|
+
|
|
311
|
+
r = self._get_raw("/v2/simple", params=params)
|
|
312
|
+
if not (200 <= r.status_code < 300):
|
|
313
|
+
return self.make_response(item, {
|
|
314
|
+
"ok": False,
|
|
315
|
+
"status": r.status_code,
|
|
316
|
+
"error": r.text,
|
|
317
|
+
"_meta": {"content_type": r.headers.get("Content-Type")},
|
|
318
|
+
})
|
|
319
|
+
# Save image
|
|
320
|
+
ext = "gif"
|
|
321
|
+
ctype = r.headers.get("Content-Type", "")
|
|
322
|
+
if "png" in ctype:
|
|
323
|
+
ext = "png"
|
|
324
|
+
elif "jpeg" in ctype or "jpg" in ctype:
|
|
325
|
+
ext = "jpg"
|
|
326
|
+
if not out:
|
|
327
|
+
fname = f"wa_simple_{self._slug(query, 60)}_{self._now()}.{ext}"
|
|
328
|
+
out = os.path.join("wolframalpha", fname)
|
|
329
|
+
local = self._save_bytes(r.content, out)
|
|
330
|
+
# register image in context
|
|
331
|
+
self._add_image(local)
|
|
332
|
+
return self.make_response(item, {
|
|
333
|
+
"ok": True,
|
|
334
|
+
"file": local,
|
|
335
|
+
"_meta": {"status": r.status_code, "content_type": ctype},
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
def cmd_wa_query(self, item: dict) -> dict:
|
|
339
|
+
p = item.get("params", {})
|
|
340
|
+
query = p.get("query") or p.get("input")
|
|
341
|
+
if not query:
|
|
342
|
+
return self.make_response(item, "Param 'query' required")
|
|
343
|
+
params: Dict[str, Any] = {
|
|
344
|
+
"appid": self._appid(),
|
|
345
|
+
"input": query,
|
|
346
|
+
"output": "json",
|
|
347
|
+
"format": p.get("format") or "plaintext,image",
|
|
348
|
+
}
|
|
349
|
+
units = self._units()
|
|
350
|
+
if units:
|
|
351
|
+
params["units"] = units
|
|
352
|
+
if p.get("podstate"):
|
|
353
|
+
params["podstate"] = p["podstate"]
|
|
354
|
+
# Timeouts
|
|
355
|
+
if p.get("scantimeout") is not None:
|
|
356
|
+
params["scantimeout"] = int(p["scantimeout"])
|
|
357
|
+
if p.get("podtimeout") is not None:
|
|
358
|
+
params["podtimeout"] = int(p["podtimeout"])
|
|
359
|
+
# Size
|
|
360
|
+
if p.get("maxwidth") is not None:
|
|
361
|
+
params["maxwidth"] = int(p["maxwidth"])
|
|
362
|
+
|
|
363
|
+
assumptions = p.get("assumptions") or []
|
|
364
|
+
base_url = f"{self._api_base()}/v2/query"
|
|
365
|
+
req_params = []
|
|
366
|
+
for k, v in params.items():
|
|
367
|
+
req_params.append((k, v))
|
|
368
|
+
if isinstance(assumptions, list):
|
|
369
|
+
for a in assumptions:
|
|
370
|
+
req_params.append(("assumption", a))
|
|
371
|
+
|
|
372
|
+
r = requests.get(base_url, headers=self._headers(), params=req_params, timeout=self._timeout())
|
|
373
|
+
data = self._handle_json(r)
|
|
374
|
+
|
|
375
|
+
# Optional image download from pods
|
|
376
|
+
if p.get("download_images"):
|
|
377
|
+
max_images = int(p.get("max_images") or 10)
|
|
378
|
+
saved: List[Dict[str, Any]] = []
|
|
379
|
+
try:
|
|
380
|
+
pods = (data.get("queryresult") or {}).get("pods") or []
|
|
381
|
+
cnt = 0
|
|
382
|
+
for pod in pods:
|
|
383
|
+
for sp in (pod.get("subpods") or []):
|
|
384
|
+
img = sp.get("img")
|
|
385
|
+
if img and img.get("src"):
|
|
386
|
+
if cnt >= max_images:
|
|
387
|
+
break
|
|
388
|
+
img_url = img["src"]
|
|
389
|
+
ir = requests.get(img_url, headers=self._headers(), timeout=self._timeout())
|
|
390
|
+
if 200 <= ir.status_code < 300:
|
|
391
|
+
ctype = ir.headers.get("Content-Type") or ""
|
|
392
|
+
ext = "png" if "png" in ctype else ("jpg" if ("jpeg" in ctype or "jpg" in ctype) else "gif")
|
|
393
|
+
fname = f"wa_pod_{self._slug(pod.get('id') or pod.get('title') or 'pod')}_{cnt}_{self._now()}.{ext}"
|
|
394
|
+
local = self._save_bytes(ir.content, os.path.join("wolframalpha", fname))
|
|
395
|
+
saved.append({"pod": pod.get("id") or pod.get("title"), "file": local})
|
|
396
|
+
# register image in context
|
|
397
|
+
self._add_image(local)
|
|
398
|
+
cnt += 1
|
|
399
|
+
if cnt >= max_images:
|
|
400
|
+
break
|
|
401
|
+
except Exception:
|
|
402
|
+
pass
|
|
403
|
+
data["_downloaded_images"] = saved
|
|
404
|
+
|
|
405
|
+
# Convenience primary plaintext
|
|
406
|
+
primary = self._extract_primary_plaintext(data)
|
|
407
|
+
if primary is not None:
|
|
408
|
+
data["_primary_plaintext"] = primary
|
|
409
|
+
|
|
410
|
+
return self.make_response(item, data)
|
|
411
|
+
|
|
412
|
+
# ---------------------- Convenience math commands ----------------------
|
|
413
|
+
|
|
414
|
+
def cmd_wa_calculate(self, item: dict) -> dict:
|
|
415
|
+
p = item.get("params", {})
|
|
416
|
+
expr = p.get("expr") or p.get("expression") or p.get("query")
|
|
417
|
+
if not expr:
|
|
418
|
+
return self.make_response(item, "Param 'expr' required")
|
|
419
|
+
# Try short answer first
|
|
420
|
+
short = self._call_cmd("wa_short", {"query": expr})
|
|
421
|
+
sdata = short.get("data") or short
|
|
422
|
+
if isinstance(sdata, dict) and sdata.get("ok"):
|
|
423
|
+
return self.make_response(item, {"expr": expr, "result": sdata.get("text"), "_via": "short"})
|
|
424
|
+
# Fallback to full JSON
|
|
425
|
+
full = self._call_cmd("wa_query", {"query": expr})
|
|
426
|
+
fdata = full.get("data") or full
|
|
427
|
+
if isinstance(fdata, dict):
|
|
428
|
+
primary = fdata.get("_primary_plaintext")
|
|
429
|
+
if primary:
|
|
430
|
+
return self.make_response(item, {"expr": expr, "result": primary, "_via": "query"})
|
|
431
|
+
return self.make_response(item, {"expr": expr, "error": "No result"})
|
|
432
|
+
|
|
433
|
+
def cmd_wa_solve(self, item: dict) -> dict:
|
|
434
|
+
p = item.get("params", {})
|
|
435
|
+
eq = p.get("equation") or p.get("eq")
|
|
436
|
+
eqs = p.get("equations") or []
|
|
437
|
+
var = p.get("var") or p.get("variable")
|
|
438
|
+
vars_ = p.get("vars") or p.get("variables") or []
|
|
439
|
+
domain = p.get("domain") # reals|integers|complexes
|
|
440
|
+
if not (eq or eqs):
|
|
441
|
+
return self.make_response(item, "Param 'equation' or 'equations' required")
|
|
442
|
+
if isinstance(eqs, list) and eq:
|
|
443
|
+
eqs = [eq] + eqs
|
|
444
|
+
elif not isinstance(eqs, list) and eq:
|
|
445
|
+
eqs = [eq]
|
|
446
|
+
if vars_ and not isinstance(vars_, list):
|
|
447
|
+
vars_ = [vars_]
|
|
448
|
+
if var and var not in vars_:
|
|
449
|
+
vars_.append(var)
|
|
450
|
+
vars_part = f" for {{{', '.join(vars_)}}}" if vars_ else ""
|
|
451
|
+
dom_part = f" over the {domain}" if domain else ""
|
|
452
|
+
query = f"solve {{{'; '.join(eqs)}}}{vars_part}{dom_part}"
|
|
453
|
+
res = self._call_cmd("wa_query", {"query": query})
|
|
454
|
+
data = res.get("data") or res
|
|
455
|
+
if isinstance(data, dict) and data.get("_primary_plaintext"):
|
|
456
|
+
return self.make_response(item, {"query": query, "solution": data["_primary_plaintext"], "_raw": data})
|
|
457
|
+
return self.make_response(item, {"query": query, "error": "No solution", "_raw": data})
|
|
458
|
+
|
|
459
|
+
def cmd_wa_derivative(self, item: dict) -> dict:
|
|
460
|
+
p = item.get("params", {})
|
|
461
|
+
expr = p.get("expr") or p.get("expression")
|
|
462
|
+
var = p.get("var") or "x"
|
|
463
|
+
order = int(p.get("order") or 1)
|
|
464
|
+
at = p.get("at") # e.g., "x=0"
|
|
465
|
+
if not expr:
|
|
466
|
+
return self.make_response(item, "Param 'expr' required")
|
|
467
|
+
q = f"derivative order {order} of ({expr}) with respect to {var}"
|
|
468
|
+
if at:
|
|
469
|
+
q += f" at {at}"
|
|
470
|
+
res = self._call_cmd("wa_query", {"query": q})
|
|
471
|
+
data = res.get("data") or res
|
|
472
|
+
if isinstance(data, dict) and data.get("_primary_plaintext"):
|
|
473
|
+
return self.make_response(item, {"query": q, "derivative": data["_primary_plaintext"], "_raw": data})
|
|
474
|
+
return self.make_response(item, {"query": q, "error": "No result", "_raw": data})
|
|
475
|
+
|
|
476
|
+
def cmd_wa_integral(self, item: dict) -> dict:
|
|
477
|
+
p = item.get("params", {})
|
|
478
|
+
expr = p.get("expr") or p.get("expression")
|
|
479
|
+
var = p.get("var") or "x"
|
|
480
|
+
a = p.get("a")
|
|
481
|
+
b = p.get("b")
|
|
482
|
+
if not expr:
|
|
483
|
+
return self.make_response(item, "Param 'expr' required")
|
|
484
|
+
if a is not None and b is not None:
|
|
485
|
+
q = f"integrate ({expr}) with respect to {var} from {a} to {b}"
|
|
486
|
+
else:
|
|
487
|
+
q = f"integrate ({expr}) with respect to {var}"
|
|
488
|
+
res = self._call_cmd("wa_query", {"query": q})
|
|
489
|
+
data = res.get("data") or res
|
|
490
|
+
if isinstance(data, dict) and data.get("_primary_plaintext"):
|
|
491
|
+
return self.make_response(item, {"query": q, "integral": data["_primary_plaintext"], "_raw": data})
|
|
492
|
+
return self.make_response(item, {"query": q, "error": "No result", "_raw": data})
|
|
493
|
+
|
|
494
|
+
def cmd_wa_units_convert(self, item: dict) -> dict:
|
|
495
|
+
p = item.get("params", {})
|
|
496
|
+
value = p.get("value")
|
|
497
|
+
from_unit = p.get("from")
|
|
498
|
+
to_unit = p.get("to")
|
|
499
|
+
if value is None or not from_unit or not to_unit:
|
|
500
|
+
return self.make_response(item, "Params 'value','from','to' required")
|
|
501
|
+
q = f"convert {value} {from_unit} to {to_unit}"
|
|
502
|
+
res = self._call_cmd("wa_query", {"query": q})
|
|
503
|
+
data = res.get("data") or res
|
|
504
|
+
if isinstance(data, dict) and data.get("_primary_plaintext"):
|
|
505
|
+
return self.make_response(item, {"query": q, "conversion": data["_primary_plaintext"], "_raw": data})
|
|
506
|
+
# Try short if no plaintext
|
|
507
|
+
short = self._call_cmd("wa_short", {"query": q})
|
|
508
|
+
sdata = short.get("data") or short
|
|
509
|
+
if isinstance(sdata, dict) and sdata.get("ok"):
|
|
510
|
+
return self.make_response(item, {"query": q, "conversion": sdata.get("text"), "_via": "short"})
|
|
511
|
+
return self.make_response(item, {"query": q, "error": "No result", "_raw": data})
|
|
512
|
+
|
|
513
|
+
def cmd_wa_matrix(self, item: dict) -> dict:
|
|
514
|
+
p = item.get("params", {})
|
|
515
|
+
op = (p.get("op") or "determinant").lower() # determinant|inverse|eigenvalues|rank
|
|
516
|
+
matrix = p.get("matrix")
|
|
517
|
+
if not (matrix and isinstance(matrix, list) and all(isinstance(r, list) for r in matrix)):
|
|
518
|
+
return self.make_response(item, "Param 'matrix' must be list of lists")
|
|
519
|
+
literal = self._matrix_literal(matrix)
|
|
520
|
+
if op == "determinant":
|
|
521
|
+
q = f"determinant {literal}"
|
|
522
|
+
elif op == "inverse":
|
|
523
|
+
q = f"inverse {literal}"
|
|
524
|
+
elif op == "eigenvalues":
|
|
525
|
+
q = f"eigenvalues {literal}"
|
|
526
|
+
elif op == "rank":
|
|
527
|
+
q = f"rank {literal}"
|
|
528
|
+
else:
|
|
529
|
+
q = f"{op} {literal}"
|
|
530
|
+
res = self._call_cmd("wa_query", {"query": q})
|
|
531
|
+
data = res.get("data") or res
|
|
532
|
+
if isinstance(data, dict) and data.get("_primary_plaintext"):
|
|
533
|
+
return self.make_response(item, {"query": q, "result": data["_primary_plaintext"], "_raw": data})
|
|
534
|
+
return self.make_response(item, {"query": q, "error": "No result", "_raw": data})
|
|
535
|
+
|
|
536
|
+
def cmd_wa_plot(self, item: dict) -> dict:
|
|
537
|
+
p = item.get("params", {})
|
|
538
|
+
func = p.get("func") or p.get("f") or p.get("function")
|
|
539
|
+
var = p.get("var") or "x"
|
|
540
|
+
a = p.get("a")
|
|
541
|
+
b = p.get("b")
|
|
542
|
+
out = p.get("out")
|
|
543
|
+
if not func:
|
|
544
|
+
return self.make_response(item, "Param 'func' required")
|
|
545
|
+
if a is not None and b is not None:
|
|
546
|
+
q = f"plot {func} for {var} from {a} to {b}"
|
|
547
|
+
else:
|
|
548
|
+
q = f"plot {func}"
|
|
549
|
+
# Use Simple API to get a ready image
|
|
550
|
+
simple_res = self._call_cmd("wa_simple", {"query": q, "out": out})
|
|
551
|
+
return self.make_response(item, simple_res.get("data") or simple_res)
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2025.09.05
|
|
9
|
+
# Updated Date: 2025.09.17 05:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import json
|
|
@@ -177,8 +177,10 @@ class Tools:
|
|
|
177
177
|
if model and model.id and model.id.startswith("claude-3-5"):
|
|
178
178
|
return tools
|
|
179
179
|
|
|
180
|
+
is_web = self.window.controller.chat.remote_tools.enabled(model, "web_search") # get global config
|
|
181
|
+
|
|
180
182
|
# Web Search tool
|
|
181
|
-
if
|
|
183
|
+
if is_web:
|
|
182
184
|
ttype = cfg.get("remote_tools.anthropic.web_search.type", "web_search_20250305") # stable as of docs
|
|
183
185
|
tname = "web_search"
|
|
184
186
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2025.09.
|
|
9
|
+
# Updated Date: 2025.09.17 05:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import os
|
|
@@ -260,9 +260,10 @@ class ApiGoogle:
|
|
|
260
260
|
tools: list = []
|
|
261
261
|
cfg = self.window.core.config
|
|
262
262
|
model_id = (model.id if model and getattr(model, "id", None) else "").lower()
|
|
263
|
+
is_web = self.window.controller.chat.remote_tools.enabled(model, "web_search") # get global config
|
|
263
264
|
|
|
264
265
|
# Google Search tool
|
|
265
|
-
if
|
|
266
|
+
if is_web and "image" not in model.id:
|
|
266
267
|
try:
|
|
267
268
|
if not model_id.startswith("gemini-1.5") and not model_id.startswith("models/gemini-1.5"):
|
|
268
269
|
# Gemini 2.x uses GoogleSearch
|
|
File without changes
|
|
@@ -13,7 +13,7 @@ from agents import (
|
|
|
13
13
|
from pygpt_net.item.model import ModelItem
|
|
14
14
|
from pygpt_net.item.preset import PresetItem
|
|
15
15
|
|
|
16
|
-
from
|
|
16
|
+
from .remote_tools import append_tools
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def get_experts(
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2025.
|
|
9
|
+
# Updated Date: 2025.09.17 05:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import json
|
|
@@ -135,9 +135,11 @@ def get_remote_tools(
|
|
|
135
135
|
"computer_use": False,
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
enabled_global = window.controller.chat.remote_tools.enabled # get global config
|
|
139
|
+
|
|
138
140
|
# from global config if not expert call
|
|
139
141
|
if not is_expert_call:
|
|
140
|
-
enabled["web_search"] =
|
|
142
|
+
enabled["web_search"] = enabled_global(model, "web_search") # <-- from global config
|
|
141
143
|
enabled["image"] = window.core.config.get("remote_tools.image", False)
|
|
142
144
|
enabled["code_interpreter"] = window.core.config.get("remote_tools.code_interpreter", False)
|
|
143
145
|
enabled["mcp"] = window.core.config.get("remote_tools.mcp", False)
|
|
@@ -147,8 +149,16 @@ def get_remote_tools(
|
|
|
147
149
|
# for expert call, get from preset config
|
|
148
150
|
if preset:
|
|
149
151
|
if preset.remote_tools:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
+
if isinstance(preset.remote_tools, str):
|
|
153
|
+
tools_list = [preset_remote_tool.strip() for preset_remote_tool in preset.remote_tools.split(",") if
|
|
154
|
+
preset_remote_tool.strip()]
|
|
155
|
+
elif isinstance(preset.remote_tools, list):
|
|
156
|
+
tools_list = [str(preset_remote_tool).strip() for preset_remote_tool in preset.remote_tools if
|
|
157
|
+
str(preset_remote_tool).strip()]
|
|
158
|
+
else:
|
|
159
|
+
tools_list = []
|
|
160
|
+
if "web_search" not in tools_list:
|
|
161
|
+
enabled["web_search"] = enabled_global(model, "web_search") # <-- from global config
|
|
152
162
|
for item in tools_list:
|
|
153
163
|
if item in enabled:
|
|
154
164
|
enabled[item] = True
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2025.
|
|
9
|
+
# Updated Date: 2025.09.17 19:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import json
|
|
@@ -145,9 +145,14 @@ class Chat:
|
|
|
145
145
|
if stream:
|
|
146
146
|
response_kwargs['stream_options'] = {"include_usage": True}
|
|
147
147
|
|
|
148
|
+
# OpenRouter: add web search remote tool (if enabled)
|
|
149
|
+
model_id = model.id
|
|
150
|
+
if model.provider == "open_router":
|
|
151
|
+
model_id = self.window.core.models.get_openrouter_model(model)
|
|
152
|
+
|
|
148
153
|
response = client.chat.completions.create(
|
|
149
154
|
messages=messages,
|
|
150
|
-
model=
|
|
155
|
+
model=model_id,
|
|
151
156
|
stream=stream,
|
|
152
157
|
**response_kwargs,
|
|
153
158
|
)
|