pygpt-net 2.6.51__py3-none-any.whl → 2.6.53__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 +10 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +3 -1
- pygpt_net/controller/__init__.py +4 -2
- 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/ctx/ctx.py +8 -3
- 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 +11 -5
- pygpt_net/controller/ui/tabs.py +31 -7
- pygpt_net/core/ctx/output.py +4 -2
- pygpt_net/core/render/web/renderer.py +5 -4
- pygpt_net/core/tabs/tab.py +42 -9
- pygpt_net/core/tabs/tabs.py +7 -9
- pygpt_net/data/config/config.json +6 -5
- 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/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 +12 -0
- pygpt_net/icons.qrc +2 -0
- pygpt_net/icons_rc.py +232 -147
- pygpt_net/plugin/mcp/__init__.py +12 -0
- pygpt_net/plugin/mcp/config.py +103 -0
- pygpt_net/plugin/mcp/plugin.py +513 -0
- pygpt_net/plugin/mcp/worker.py +263 -0
- pygpt_net/provider/api/anthropic/tools.py +4 -2
- pygpt_net/provider/api/google/__init__.py +3 -2
- pygpt_net/provider/api/openai/agents/remote_tools.py +14 -4
- pygpt_net/provider/api/openai/chat.py +14 -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/ui/dialog/plugins.py +1 -3
- pygpt_net/ui/layout/chat/output.py +7 -2
- pygpt_net/ui/widget/element/labels.py +1 -2
- pygpt_net/ui/widget/tabs/body.py +24 -5
- {pygpt_net-2.6.51.dist-info → pygpt_net-2.6.53.dist-info}/METADATA +24 -4
- {pygpt_net-2.6.51.dist-info → pygpt_net-2.6.53.dist-info}/RECORD +53 -45
- {pygpt_net-2.6.51.dist-info → pygpt_net-2.6.53.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.51.dist-info → pygpt_net-2.6.53.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.51.dist-info → pygpt_net-2.6.53.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,263 @@
|
|
|
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.16 22:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import shlex
|
|
15
|
+
from contextlib import asynccontextmanager
|
|
16
|
+
from typing import Dict, Any, List, Tuple, Optional
|
|
17
|
+
from urllib.parse import urlparse
|
|
18
|
+
|
|
19
|
+
from PySide6.QtCore import Slot
|
|
20
|
+
|
|
21
|
+
from pygpt_net.plugin.base.worker import BaseWorker, BaseSignals
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WorkerSignals(BaseSignals):
|
|
25
|
+
pass # add custom signals here
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Worker(BaseWorker):
|
|
29
|
+
def __init__(self, *args, **kwargs):
|
|
30
|
+
super(Worker, self).__init__()
|
|
31
|
+
self.signals = BaseSignals()
|
|
32
|
+
self.args = args
|
|
33
|
+
self.kwargs = kwargs
|
|
34
|
+
self.plugin = None
|
|
35
|
+
self.cmds: Optional[List[dict]] = None
|
|
36
|
+
self.ctx = None
|
|
37
|
+
self.tools_index: Dict[str, Dict[str, Any]] = {}
|
|
38
|
+
|
|
39
|
+
@Slot()
|
|
40
|
+
def run(self):
|
|
41
|
+
"""
|
|
42
|
+
Worker entry point executed in a background thread.
|
|
43
|
+
Starts an asyncio loop to talk to MCP servers.
|
|
44
|
+
"""
|
|
45
|
+
try:
|
|
46
|
+
responses = asyncio.run(self._run_async())
|
|
47
|
+
if responses:
|
|
48
|
+
self.reply_more(responses)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
self.error(e)
|
|
51
|
+
finally:
|
|
52
|
+
self.cleanup()
|
|
53
|
+
|
|
54
|
+
async def _run_async(self) -> List[dict]:
|
|
55
|
+
"""
|
|
56
|
+
Group commands per server, open a session per server, then call tools.
|
|
57
|
+
"""
|
|
58
|
+
responses: List[dict] = []
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
from mcp import ClientSession, types # type: ignore
|
|
62
|
+
from mcp.client.stdio import stdio_client # type: ignore
|
|
63
|
+
from mcp.client.streamable_http import streamablehttp_client # type: ignore
|
|
64
|
+
from mcp.client.sse import sse_client # type: ignore
|
|
65
|
+
from mcp import StdioServerParameters # type: ignore
|
|
66
|
+
except Exception as e:
|
|
67
|
+
self.status('MCP SDK not installed. Install with: pip install "mcp[cli]"')
|
|
68
|
+
self.log(f"MCP import error in worker: {e}")
|
|
69
|
+
for item in (self.cmds or []):
|
|
70
|
+
responses.append(self.make_response(item, f"MCP SDK not installed: {e}"))
|
|
71
|
+
return responses
|
|
72
|
+
|
|
73
|
+
# Group by server
|
|
74
|
+
grouped: Dict[str, List[dict]] = {}
|
|
75
|
+
for item in self.cmds or []:
|
|
76
|
+
meta = self.tools_index.get(item["cmd"])
|
|
77
|
+
if not meta:
|
|
78
|
+
continue
|
|
79
|
+
server_key = self._server_key(meta["server"])
|
|
80
|
+
grouped.setdefault(server_key, []).append(item)
|
|
81
|
+
|
|
82
|
+
# Execute per server
|
|
83
|
+
for server_key, items in grouped.items():
|
|
84
|
+
meta0 = self.tools_index.get(items[0]["cmd"])
|
|
85
|
+
if not meta0:
|
|
86
|
+
continue
|
|
87
|
+
server_cfg = meta0["server"]
|
|
88
|
+
address = (server_cfg.get("server_address") or "").strip()
|
|
89
|
+
transport = meta0["transport"]
|
|
90
|
+
headers = self._build_headers(server_cfg)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
async with self._open_session(address, transport, headers=headers) as session:
|
|
94
|
+
for item in items:
|
|
95
|
+
if self.is_stopped():
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
meta = self.tools_index.get(item["cmd"])
|
|
99
|
+
if not meta:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
tool_name = meta["tool_name"]
|
|
103
|
+
schema = meta.get("schema")
|
|
104
|
+
arguments = self._coerce_arguments(item.get("params", {}), schema)
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
result = await session.call_tool(tool_name, arguments=arguments)
|
|
108
|
+
text = self._extract_text_result(result)
|
|
109
|
+
responses.append(self.make_response(item, text))
|
|
110
|
+
except Exception as e:
|
|
111
|
+
responses.append(self.make_response(item, self.throw_error(e)))
|
|
112
|
+
|
|
113
|
+
except Exception as e:
|
|
114
|
+
msg = f"MCP server error ({address}): {e}"
|
|
115
|
+
self.log(msg)
|
|
116
|
+
self.status(msg)
|
|
117
|
+
for item in items:
|
|
118
|
+
responses.append(self.make_response(item, self.throw_error(e)))
|
|
119
|
+
|
|
120
|
+
return responses
|
|
121
|
+
|
|
122
|
+
# ---------------------------
|
|
123
|
+
# Session / transport helpers
|
|
124
|
+
# ---------------------------
|
|
125
|
+
|
|
126
|
+
@asynccontextmanager
|
|
127
|
+
async def _open_session(self, address: str, transport: str, headers: Optional[dict] = None):
|
|
128
|
+
"""
|
|
129
|
+
Open and initialize MCP session for given server address and transport.
|
|
130
|
+
Yields a ready-to-use ClientSession.
|
|
131
|
+
"""
|
|
132
|
+
from mcp import ClientSession # type: ignore
|
|
133
|
+
|
|
134
|
+
if transport == "stdio":
|
|
135
|
+
from mcp.client.stdio import stdio_client # type: ignore
|
|
136
|
+
from mcp import StdioServerParameters # type: ignore
|
|
137
|
+
cmd, args = self._parse_stdio_command(address)
|
|
138
|
+
params = StdioServerParameters(command=cmd, args=args)
|
|
139
|
+
async with stdio_client(params) as (read, write):
|
|
140
|
+
async with ClientSession(read, write) as session:
|
|
141
|
+
await session.initialize()
|
|
142
|
+
yield session
|
|
143
|
+
|
|
144
|
+
elif transport == "http":
|
|
145
|
+
from mcp.client.streamable_http import streamablehttp_client # type: ignore
|
|
146
|
+
async with streamablehttp_client(address, headers=headers or None) as (read, write, _):
|
|
147
|
+
async with ClientSession(read, write) as session:
|
|
148
|
+
await session.initialize()
|
|
149
|
+
yield session
|
|
150
|
+
|
|
151
|
+
elif transport == "sse":
|
|
152
|
+
from mcp.client.sse import sse_client # type: ignore
|
|
153
|
+
async with sse_client(address, headers=headers or None) as (read, write):
|
|
154
|
+
async with ClientSession(read, write) as session:
|
|
155
|
+
await session.initialize()
|
|
156
|
+
yield session
|
|
157
|
+
|
|
158
|
+
else:
|
|
159
|
+
raise RuntimeError(f"Unsupported transport: {transport}")
|
|
160
|
+
|
|
161
|
+
def _parse_stdio_command(self, address: str) -> Tuple[str, List[str]]:
|
|
162
|
+
"""Parse 'stdio: <command line>' into (command, args)."""
|
|
163
|
+
cmdline = address[len("stdio:"):].strip()
|
|
164
|
+
tokens = shlex.split(cmdline)
|
|
165
|
+
if not tokens:
|
|
166
|
+
raise ValueError("Invalid stdio address: empty command")
|
|
167
|
+
return tokens[0], tokens[1:]
|
|
168
|
+
|
|
169
|
+
# ---------------------------
|
|
170
|
+
# Result & argument handling
|
|
171
|
+
# ---------------------------
|
|
172
|
+
|
|
173
|
+
def _coerce_arguments(self, params: Dict[str, Any], schema: Optional[dict]) -> Dict[str, Any]:
|
|
174
|
+
"""Coerce incoming params into types expected by the tool schema."""
|
|
175
|
+
if not schema or not isinstance(schema, dict):
|
|
176
|
+
return params or {}
|
|
177
|
+
|
|
178
|
+
props = schema.get("properties", {}) or {}
|
|
179
|
+
coerced: Dict[str, Any] = {}
|
|
180
|
+
|
|
181
|
+
for name, value in (params or {}).items():
|
|
182
|
+
target = props.get(name, {})
|
|
183
|
+
jtype = target.get("type", "string")
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
if jtype == "integer":
|
|
187
|
+
coerced[name] = int(value)
|
|
188
|
+
elif jtype == "number":
|
|
189
|
+
coerced[name] = float(value)
|
|
190
|
+
elif jtype == "boolean":
|
|
191
|
+
if isinstance(value, bool):
|
|
192
|
+
coerced[name] = value
|
|
193
|
+
elif isinstance(value, str):
|
|
194
|
+
coerced[name] = value.strip().lower() in ("1", "true", "yes", "y", "on")
|
|
195
|
+
else:
|
|
196
|
+
coerced[name] = bool(value)
|
|
197
|
+
elif jtype in ("array", "object"):
|
|
198
|
+
if isinstance(value, (dict, list)):
|
|
199
|
+
coerced[name] = value
|
|
200
|
+
elif isinstance(value, str):
|
|
201
|
+
try:
|
|
202
|
+
coerced[name] = json.loads(value)
|
|
203
|
+
except Exception:
|
|
204
|
+
coerced[name] = value
|
|
205
|
+
else:
|
|
206
|
+
coerced[name] = value
|
|
207
|
+
else:
|
|
208
|
+
coerced[name] = value
|
|
209
|
+
except Exception:
|
|
210
|
+
coerced[name] = value
|
|
211
|
+
|
|
212
|
+
return coerced
|
|
213
|
+
|
|
214
|
+
def _extract_text_result(self, result: Any) -> str:
|
|
215
|
+
"""
|
|
216
|
+
Convert MCP tool result into a readable string.
|
|
217
|
+
Prefer structuredContent; fallback to text content blocks.
|
|
218
|
+
"""
|
|
219
|
+
try:
|
|
220
|
+
if getattr(result, "structuredContent", None) is not None:
|
|
221
|
+
return json.dumps(result.structuredContent, ensure_ascii=False, indent=2)
|
|
222
|
+
|
|
223
|
+
content_list = getattr(result, "content", None)
|
|
224
|
+
if not content_list:
|
|
225
|
+
return "No result (empty content)"
|
|
226
|
+
from mcp import types # type: ignore
|
|
227
|
+
texts: List[str] = []
|
|
228
|
+
for block in content_list:
|
|
229
|
+
if isinstance(block, types.TextContent):
|
|
230
|
+
texts.append(block.text)
|
|
231
|
+
else:
|
|
232
|
+
btype = getattr(block, "type", "content")
|
|
233
|
+
texts.append(f"[{btype}]")
|
|
234
|
+
return "\n".join(texts) if texts else "No result (no text content)"
|
|
235
|
+
except Exception as e:
|
|
236
|
+
return f"Failed to parse MCP result: {e}"
|
|
237
|
+
|
|
238
|
+
# ---------------------------
|
|
239
|
+
# Misc helpers
|
|
240
|
+
# ---------------------------
|
|
241
|
+
|
|
242
|
+
def _server_key(self, server: dict) -> str:
|
|
243
|
+
"""Create a deterministic key for a server config entry."""
|
|
244
|
+
addr = (server.get("server_address") or "").strip()
|
|
245
|
+
if addr.lower().startswith("http"):
|
|
246
|
+
try:
|
|
247
|
+
parsed = urlparse(addr)
|
|
248
|
+
return f"http::{parsed.netloc}{parsed.path}"
|
|
249
|
+
except Exception:
|
|
250
|
+
return f"http::{addr}"
|
|
251
|
+
if addr.lower().startswith(("sse://", "sse+http://", "sse+https://")):
|
|
252
|
+
return f"sse::{addr}"
|
|
253
|
+
if addr.startswith("stdio:"):
|
|
254
|
+
return f"stdio::{addr[len('stdio:'):].strip()}"
|
|
255
|
+
return addr
|
|
256
|
+
|
|
257
|
+
def _build_headers(self, server: dict) -> Optional[dict]:
|
|
258
|
+
"""Build optional headers for HTTP/SSE transports (Authorization only)."""
|
|
259
|
+
auth = (server.get("authorization") or "").strip()
|
|
260
|
+
headers = {}
|
|
261
|
+
if auth:
|
|
262
|
+
headers["Authorization"] = auth
|
|
263
|
+
return headers or None
|
|
@@ -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
|
|
@@ -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 05:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import json
|
|
@@ -145,9 +145,21 @@ 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
|
+
# https://openrouter.ai/docs/features/web-search
|
|
150
|
+
model_id = model.id
|
|
151
|
+
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", "")
|
|
159
|
+
|
|
148
160
|
response = client.chat.completions.create(
|
|
149
161
|
messages=messages,
|
|
150
|
-
model=
|
|
162
|
+
model=model_id,
|
|
151
163
|
stream=stream,
|
|
152
164
|
**response_kwargs,
|
|
153
165
|
)
|
|
@@ -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
|
|
@@ -87,10 +87,11 @@ class RemoteTools:
|
|
|
87
87
|
"file_search": False,
|
|
88
88
|
"computer_use": False,
|
|
89
89
|
}
|
|
90
|
+
enabled_global = self.window.controller.chat.remote_tools.enabled # get global config
|
|
90
91
|
|
|
91
92
|
# from global config if not expert call
|
|
92
93
|
if not is_expert_call:
|
|
93
|
-
enabled["web_search"] =
|
|
94
|
+
enabled["web_search"] = enabled_global(model, "web_search") # <-- from global config
|
|
94
95
|
enabled["image"] = self.window.core.config.get("remote_tools.image", False)
|
|
95
96
|
enabled["code_interpreter"] = self.window.core.config.get("remote_tools.code_interpreter", False)
|
|
96
97
|
enabled["mcp"] = self.window.core.config.get("remote_tools.mcp", False)
|
|
@@ -101,6 +102,8 @@ class RemoteTools:
|
|
|
101
102
|
if preset:
|
|
102
103
|
if preset.remote_tools:
|
|
103
104
|
tools_list = [preset_remote_tool.strip() for preset_remote_tool in preset.remote_tools.split(",") if preset_remote_tool.strip()]
|
|
105
|
+
if "web_search" not in tools_list:
|
|
106
|
+
enabled["web_search"] = enabled_global(model, "web_search") # <-- from global config
|
|
104
107
|
for item in tools_list:
|
|
105
108
|
if item in enabled:
|
|
106
109
|
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.09.05
|
|
9
|
+
# Updated Date: 2025.09.17 05:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
@@ -57,11 +57,16 @@ class Remote:
|
|
|
57
57
|
:return: Dict with 'sdk' and 'http' keys
|
|
58
58
|
"""
|
|
59
59
|
cfg = self.window.core.config
|
|
60
|
+
is_web = self.window.controller.chat.remote_tools.enabled(model, "web_search") # get global config
|
|
60
61
|
|
|
61
62
|
mode = str(cfg.get("remote_tools.xai.mode") or "auto").lower()
|
|
62
63
|
if mode not in ("auto", "on", "off"):
|
|
63
64
|
mode = "auto"
|
|
64
65
|
|
|
66
|
+
if mode == "off":
|
|
67
|
+
if is_web:
|
|
68
|
+
mode = "auto" # override off if global web_search enabled
|
|
69
|
+
|
|
65
70
|
# sources toggles
|
|
66
71
|
s_web = bool(cfg.get("remote_tools.xai.sources.web", True))
|
|
67
72
|
s_x = bool(cfg.get("remote_tools.xai.sources.x", 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 05:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import copy
|
|
@@ -104,6 +104,13 @@ class Patch:
|
|
|
104
104
|
patch_css('style.dark.css', True)
|
|
105
105
|
updated = True
|
|
106
106
|
|
|
107
|
+
# < 2.6.53
|
|
108
|
+
if old < parse_version("2.6.53"):
|
|
109
|
+
print("Migrating config from < 2.6.53...")
|
|
110
|
+
if "remote_tools.global.web_search" not in data:
|
|
111
|
+
data["remote_tools.global.web_search"] = True
|
|
112
|
+
updated = True
|
|
113
|
+
|
|
107
114
|
# update file
|
|
108
115
|
migrated = False
|
|
109
116
|
if updated:
|
pygpt_net/ui/dialog/plugins.py
CHANGED
|
@@ -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.16 22:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtCore import Qt
|
|
@@ -454,7 +454,6 @@ class Plugins:
|
|
|
454
454
|
# cols_widget.setMaximumHeight(90)
|
|
455
455
|
|
|
456
456
|
self.window.ui.nodes[desc_key] = DescLabel(txt_desc)
|
|
457
|
-
self.window.ui.nodes[desc_key].setMaximumHeight(40)
|
|
458
457
|
# self.window.ui.nodes[desc_key].setToolTip(txt_tooltip)
|
|
459
458
|
|
|
460
459
|
layout = QVBoxLayout()
|
|
@@ -473,7 +472,6 @@ class Plugins:
|
|
|
473
472
|
|
|
474
473
|
if option['type'] not in no_desc_types:
|
|
475
474
|
self.window.ui.nodes[desc_key] = DescLabel(txt_desc)
|
|
476
|
-
self.window.ui.nodes[desc_key].setMaximumHeight(40)
|
|
477
475
|
# self.window.ui.nodes[desc_key].setToolTip(txt_tooltip)
|
|
478
476
|
layout.addWidget(self.window.ui.nodes[desc_key])
|
|
479
477
|
|
|
@@ -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 07:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtCore import Qt
|
|
@@ -72,7 +72,7 @@ class Output:
|
|
|
72
72
|
|
|
73
73
|
nodes['icon.audio.output'] = IconLabel(":/icons/volume.svg", window=self.window)
|
|
74
74
|
nodes['icon.audio.output'].setToolTip(trans("icon.audio.output"))
|
|
75
|
-
nodes['icon.audio.output'].clicked.connect(lambda: ctrl.plugins.
|
|
75
|
+
nodes['icon.audio.output'].clicked.connect(lambda: ctrl.plugins.toggle_audio_output()) # special case for AUDIO mode
|
|
76
76
|
|
|
77
77
|
nodes['icon.audio.input'] = IconLabel(":/icons/mic.svg", window=self.window)
|
|
78
78
|
nodes['icon.audio.input'].setToolTip(trans("icon.audio.input"))
|
|
@@ -86,6 +86,10 @@ class Output:
|
|
|
86
86
|
nodes['icon.indexer'].setToolTip("Indexer")
|
|
87
87
|
nodes['icon.indexer'].clicked.connect(lambda: tools.get("indexer").toggle())
|
|
88
88
|
|
|
89
|
+
nodes['icon.remote_tool.web'] = IconLabel(":/icons/web_on.svg", window=self.window)
|
|
90
|
+
nodes['icon.remote_tool.web'].setToolTip(trans("icon.remote_tool.web"))
|
|
91
|
+
nodes['icon.remote_tool.web'].clicked.connect(lambda: ctrl.chat.remote_tools.toggle('web_search'))
|
|
92
|
+
|
|
89
93
|
min_policy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
|
|
90
94
|
|
|
91
95
|
nodes['chat.label'] = ChatStatusLabel("")
|
|
@@ -135,6 +139,7 @@ class Output:
|
|
|
135
139
|
nodes['anim.loading'].hide()
|
|
136
140
|
|
|
137
141
|
right_bar_layout = QHBoxLayout()
|
|
142
|
+
right_bar_layout.addWidget(nodes['icon.remote_tool.web'])
|
|
138
143
|
right_bar_layout.addWidget(nodes['icon.video.capture'])
|
|
139
144
|
right_bar_layout.addWidget(nodes['icon.audio.input'])
|
|
140
145
|
right_bar_layout.addWidget(nodes['icon.audio.output'])
|
|
@@ -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.16 22:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtCore import Qt, QTimer, QRect, Signal, QUrl, QEvent
|
|
@@ -34,7 +34,6 @@ class DescLabel(BaseLabel):
|
|
|
34
34
|
def __init__(self, text, window=None):
|
|
35
35
|
super().__init__(text, window)
|
|
36
36
|
self.window = window
|
|
37
|
-
self.setMaximumHeight(80)
|
|
38
37
|
self.setProperty('class', 'label-desc')
|
|
39
38
|
|
|
40
39
|
|
pygpt_net/ui/widget/tabs/body.py
CHANGED
|
@@ -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.16
|
|
9
|
+
# Updated Date: 2025.09.16 22:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from typing import Any
|
|
@@ -35,7 +35,7 @@ class TabBody(QTabWidget):
|
|
|
35
35
|
self.on_delete(self)
|
|
36
36
|
self.delete_refs()
|
|
37
37
|
|
|
38
|
-
def
|
|
38
|
+
def unwrap(self, widget: QWidget) -> None:
|
|
39
39
|
"""
|
|
40
40
|
Remove widget from tab body
|
|
41
41
|
|
|
@@ -46,7 +46,7 @@ class TabBody(QTabWidget):
|
|
|
46
46
|
layout.removeWidget(widget)
|
|
47
47
|
self.delete_ref(widget)
|
|
48
48
|
|
|
49
|
-
def
|
|
49
|
+
def unwrap_all(self) -> None:
|
|
50
50
|
"""
|
|
51
51
|
Remove all widgets from tab body
|
|
52
52
|
"""
|
|
@@ -55,7 +55,14 @@ class TabBody(QTabWidget):
|
|
|
55
55
|
while layout.count():
|
|
56
56
|
item = layout.takeAt(0)
|
|
57
57
|
widget = item.widget()
|
|
58
|
-
|
|
58
|
+
try:
|
|
59
|
+
layout.removeWidget(widget)
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
try:
|
|
63
|
+
self.delete_ref(widget)
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
59
66
|
|
|
60
67
|
def add_ref(self, ref: Any) -> None:
|
|
61
68
|
"""
|
|
@@ -157,4 +164,16 @@ class TabBody(QTabWidget):
|
|
|
157
164
|
if self.owner is not None:
|
|
158
165
|
col_idx = self.owner.column_idx
|
|
159
166
|
self.window.controller.ui.tabs.on_column_focus(col_idx)
|
|
160
|
-
return super().eventFilter(source, event)
|
|
167
|
+
return super().eventFilter(source, event)
|
|
168
|
+
|
|
169
|
+
def to_dict(self) -> dict:
|
|
170
|
+
"""
|
|
171
|
+
Convert to dict
|
|
172
|
+
|
|
173
|
+
:return: dict
|
|
174
|
+
"""
|
|
175
|
+
return {
|
|
176
|
+
"refs": [str(ref) for ref in self.refs], # references to widgets
|
|
177
|
+
"body": [str(b) for b in self.body], # body widgets
|
|
178
|
+
"len(layout)": self.layout().count() if self.layout() else 0,
|
|
179
|
+
}
|