pygpt-net 2.6.53__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 +6 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +4 -0
- pygpt_net/controller/chat/remote_tools.py +2 -2
- pygpt_net/controller/ui/mode.py +7 -1
- pygpt_net/core/agents/provider.py +16 -9
- pygpt_net/core/models/models.py +25 -1
- pygpt_net/data/config/config.json +4 -4
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/js/app.js +19 -0
- pygpt_net/data/locale/plugin.osm.en.ini +35 -0
- pygpt_net/data/locale/plugin.wolfram.en.ini +24 -0
- 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/google/video.py +0 -0
- pygpt_net/provider/api/openai/agents/experts.py +1 -1
- pygpt_net/provider/api/openai/chat.py +2 -9
- pygpt_net/provider/api/x_ai/remote.py +2 -2
- 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-2.6.53.dist-info → pygpt_net-2.6.54.dist-info}/METADATA +32 -2
- {pygpt_net-2.6.53.dist-info → pygpt_net-2.6.54.dist-info}/RECORD +34 -24
- {pygpt_net-2.6.53.dist-info → pygpt_net-2.6.54.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.53.dist-info → pygpt_net-2.6.54.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.53.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)
|
|
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.09.17
|
|
9
|
+
# Updated Date: 2025.09.17 19:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import json
|
|
@@ -146,16 +146,9 @@ class Chat:
|
|
|
146
146
|
response_kwargs['stream_options'] = {"include_usage": True}
|
|
147
147
|
|
|
148
148
|
# OpenRouter: add web search remote tool (if enabled)
|
|
149
|
-
# https://openrouter.ai/docs/features/web-search
|
|
150
149
|
model_id = model.id
|
|
151
150
|
if model.provider == "open_router":
|
|
152
|
-
|
|
153
|
-
if is_web:
|
|
154
|
-
if not model_id.endswith(":online"):
|
|
155
|
-
model_id += ":online"
|
|
156
|
-
else:
|
|
157
|
-
if model_id.endswith(":online"):
|
|
158
|
-
model_id = model_id.replace(":online", "")
|
|
151
|
+
model_id = self.window.core.models.get_openrouter_model(model)
|
|
159
152
|
|
|
160
153
|
response = client.chat.completions.create(
|
|
161
154
|
messages=messages,
|
|
@@ -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.17
|
|
9
|
+
# Updated Date: 2025.09.17 20:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
@@ -65,7 +65,7 @@ class Remote:
|
|
|
65
65
|
|
|
66
66
|
if mode == "off":
|
|
67
67
|
if is_web:
|
|
68
|
-
mode = "
|
|
68
|
+
mode = "on" # override off if global web_search enabled
|
|
69
69
|
|
|
70
70
|
# sources toggles
|
|
71
71
|
s_web = bool(cfg.get("remote_tools.xai.sources.web", 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.09.
|
|
9
|
+
# Updated Date: 2025.09.17 20:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from typing import List, Dict, Optional
|
|
@@ -78,6 +78,34 @@ class AnthropicLLM(BaseLLM):
|
|
|
78
78
|
if "api_key" not in args or args["api_key"] == "":
|
|
79
79
|
args["api_key"] = window.core.config.get("api_key_anthropic", "")
|
|
80
80
|
|
|
81
|
+
# ---------------------------------------------
|
|
82
|
+
# Remote server tools (e.g., web_search_20250305)
|
|
83
|
+
# We forward provider-native server tools via Anthropic "tools" param.
|
|
84
|
+
# This keeps behavior identical to the native SDK configuration.
|
|
85
|
+
# ---------------------------------------------
|
|
86
|
+
try:
|
|
87
|
+
remote_tools = window.core.api.anthropic.tools.build_remote_tools(model=model) or []
|
|
88
|
+
except Exception as e:
|
|
89
|
+
# Do not break if config builder throws; just skip tools
|
|
90
|
+
window.core.debug.log(e)
|
|
91
|
+
remote_tools = []
|
|
92
|
+
|
|
93
|
+
if remote_tools:
|
|
94
|
+
# Merge with any user-supplied 'tools' (avoid duplicates by (type, name))
|
|
95
|
+
existing = args.get("tools") or []
|
|
96
|
+
if isinstance(existing, list):
|
|
97
|
+
def _key(d: dict) -> str:
|
|
98
|
+
return f"{d.get('type')}::{d.get('name')}"
|
|
99
|
+
index = {_key(t): True for t in existing if isinstance(t, dict)}
|
|
100
|
+
for t in remote_tools:
|
|
101
|
+
k = _key(t) if isinstance(t, dict) else None
|
|
102
|
+
if k and k not in index:
|
|
103
|
+
existing.append(t)
|
|
104
|
+
args["tools"] = existing
|
|
105
|
+
else:
|
|
106
|
+
# Defensive: if 'tools' was something unexpected, overwrite safely
|
|
107
|
+
args["tools"] = list(remote_tools)
|
|
108
|
+
|
|
81
109
|
return AnthropicWithProxy(**args, proxy=proxy)
|
|
82
110
|
|
|
83
111
|
def get_embeddings_model(
|