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.
Files changed (35) hide show
  1. pygpt_net/CHANGELOG.txt +6 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +4 -0
  4. pygpt_net/controller/chat/remote_tools.py +2 -2
  5. pygpt_net/controller/ui/mode.py +7 -1
  6. pygpt_net/core/agents/provider.py +16 -9
  7. pygpt_net/core/models/models.py +25 -1
  8. pygpt_net/data/config/config.json +4 -4
  9. pygpt_net/data/config/models.json +3 -3
  10. pygpt_net/data/js/app.js +19 -0
  11. pygpt_net/data/locale/plugin.osm.en.ini +35 -0
  12. pygpt_net/data/locale/plugin.wolfram.en.ini +24 -0
  13. pygpt_net/js_rc.py +10490 -10432
  14. pygpt_net/plugin/base/worker.py +7 -1
  15. pygpt_net/plugin/osm/__init__.py +12 -0
  16. pygpt_net/plugin/osm/config.py +267 -0
  17. pygpt_net/plugin/osm/plugin.py +87 -0
  18. pygpt_net/plugin/osm/worker.py +719 -0
  19. pygpt_net/plugin/wolfram/__init__.py +12 -0
  20. pygpt_net/plugin/wolfram/config.py +214 -0
  21. pygpt_net/plugin/wolfram/plugin.py +115 -0
  22. pygpt_net/plugin/wolfram/worker.py +551 -0
  23. pygpt_net/provider/api/google/video.py +0 -0
  24. pygpt_net/provider/api/openai/agents/experts.py +1 -1
  25. pygpt_net/provider/api/openai/chat.py +2 -9
  26. pygpt_net/provider/api/x_ai/remote.py +2 -2
  27. pygpt_net/provider/llms/anthropic.py +29 -1
  28. pygpt_net/provider/llms/google.py +30 -1
  29. pygpt_net/provider/llms/open_router.py +3 -1
  30. pygpt_net/provider/llms/x_ai.py +21 -1
  31. {pygpt_net-2.6.53.dist-info → pygpt_net-2.6.54.dist-info}/METADATA +32 -2
  32. {pygpt_net-2.6.53.dist-info → pygpt_net-2.6.54.dist-info}/RECORD +34 -24
  33. {pygpt_net-2.6.53.dist-info → pygpt_net-2.6.54.dist-info}/LICENSE +0 -0
  34. {pygpt_net-2.6.53.dist-info → pygpt_net-2.6.54.dist-info}/WHEEL +0 -0
  35. {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 pygpt_net.provider.api.openai.agents.remote_tools import append_tools
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 05:00:00 #
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
- is_web = self.window.controller.chat.remote_tools.enabled(model, "web_search") # web search config
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 05:00:00 #
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 = "auto" # override off if global web_search enabled
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.15 01:00:00 #
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(