pygpt-net 2.7.6__py3-none-any.whl → 2.7.7__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/controller/chat/remote_tools.py +3 -9
- pygpt_net/controller/chat/stream.py +2 -2
- pygpt_net/controller/chat/{handler/worker.py → stream_worker.py} +13 -35
- pygpt_net/core/debug/models.py +2 -2
- pygpt_net/data/config/config.json +14 -4
- pygpt_net/data/config/models.json +192 -4
- pygpt_net/data/config/settings.json +125 -35
- pygpt_net/data/locale/locale.de.ini +2 -0
- pygpt_net/data/locale/locale.en.ini +32 -8
- pygpt_net/data/locale/locale.es.ini +2 -0
- pygpt_net/data/locale/locale.fr.ini +2 -0
- pygpt_net/data/locale/locale.it.ini +2 -0
- pygpt_net/data/locale/locale.pl.ini +3 -1
- pygpt_net/data/locale/locale.uk.ini +2 -0
- pygpt_net/data/locale/locale.zh.ini +2 -0
- pygpt_net/plugin/cmd_mouse_control/worker.py +2 -1
- pygpt_net/plugin/cmd_mouse_control/worker_sandbox.py +2 -1
- pygpt_net/provider/api/anthropic/__init__.py +8 -3
- pygpt_net/provider/api/anthropic/chat.py +259 -11
- pygpt_net/provider/api/anthropic/computer.py +844 -0
- pygpt_net/provider/api/anthropic/remote_tools.py +172 -0
- pygpt_net/{controller/chat/handler/anthropic_stream.py → provider/api/anthropic/stream.py} +24 -10
- pygpt_net/provider/api/anthropic/tools.py +32 -77
- pygpt_net/provider/api/anthropic/utils.py +30 -0
- pygpt_net/provider/api/google/chat.py +3 -7
- pygpt_net/{controller/chat/handler/google_stream.py → provider/api/google/stream.py} +1 -1
- pygpt_net/provider/api/google/utils.py +185 -0
- pygpt_net/{controller/chat/handler → provider/api/langchain}/__init__.py +0 -0
- pygpt_net/{controller/chat/handler/langchain_stream.py → provider/api/langchain/stream.py} +1 -1
- pygpt_net/provider/api/llama_index/__init__.py +0 -0
- pygpt_net/{controller/chat/handler/llamaindex_stream.py → provider/api/llama_index/stream.py} +1 -1
- pygpt_net/provider/api/openai/image.py +2 -2
- pygpt_net/{controller/chat/handler/openai_stream.py → provider/api/openai/stream.py} +1 -1
- pygpt_net/provider/api/openai/utils.py +69 -3
- pygpt_net/provider/api/x_ai/__init__.py +109 -10
- pygpt_net/provider/api/x_ai/chat.py +0 -0
- pygpt_net/provider/api/x_ai/image.py +149 -47
- pygpt_net/provider/api/x_ai/{remote.py → remote_tools.py} +165 -70
- pygpt_net/provider/api/x_ai/responses.py +507 -0
- pygpt_net/{controller/chat/handler/xai_stream.py → provider/api/x_ai/stream.py} +12 -1
- pygpt_net/provider/api/x_ai/tools.py +59 -8
- pygpt_net/{controller/chat/handler → provider/api/x_ai}/utils.py +1 -2
- pygpt_net/provider/api/x_ai/vision.py +1 -4
- pygpt_net/provider/core/config/patch.py +22 -1
- pygpt_net/provider/core/model/patch.py +26 -1
- pygpt_net/tools/image_viewer/ui/dialogs.py +3 -2
- pygpt_net/tools/text_editor/ui/dialogs.py +3 -2
- pygpt_net/tools/text_editor/ui/widgets.py +0 -0
- pygpt_net/ui/widget/dialog/base.py +16 -5
- pygpt_net/ui/widget/textarea/editor.py +0 -0
- {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.7.dist-info}/METADATA +8 -2
- {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.7.dist-info}/RECORD +54 -48
- {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.7.dist-info}/LICENSE +0 -0
- {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.7.dist-info}/WHEEL +0 -0
- {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.7.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,172 @@
|
|
|
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: 2026.01.05 20:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from typing import List, Any, Dict, Optional
|
|
14
|
+
|
|
15
|
+
from pygpt_net.item.model import ModelItem
|
|
16
|
+
|
|
17
|
+
class RemoteTools:
|
|
18
|
+
def __init__(self, window=None):
|
|
19
|
+
"""
|
|
20
|
+
Remote tools mapper for Anthropic Messages API.
|
|
21
|
+
|
|
22
|
+
:param window: Window instance
|
|
23
|
+
"""
|
|
24
|
+
self.window = window
|
|
25
|
+
|
|
26
|
+
def build_remote_tools(self, model: ModelItem = None) -> List[dict]:
|
|
27
|
+
"""
|
|
28
|
+
Build Anthropic server tools (remote tools) based on config flags.
|
|
29
|
+
Supports: Web Search, Code Execution, Web Fetch, Tool Search, MCP toolset.
|
|
30
|
+
|
|
31
|
+
Returns a list of tool dicts to be appended to 'tools' in messages.create.
|
|
32
|
+
|
|
33
|
+
:param model: ModelItem
|
|
34
|
+
:return: List of remote tool dicts
|
|
35
|
+
"""
|
|
36
|
+
cfg = self.window.core.config
|
|
37
|
+
tools: List[dict] = []
|
|
38
|
+
|
|
39
|
+
# keep compatibility with previous models that had no remote tool support
|
|
40
|
+
if model and model.id and model.id.startswith("claude-3-5"):
|
|
41
|
+
# remote tool availability on 3.5 models varies; previous behavior was to skip
|
|
42
|
+
return tools
|
|
43
|
+
|
|
44
|
+
# Helper: bool from config with provider-specific fallback
|
|
45
|
+
def cfg_bool(*keys: str, default: bool = False) -> bool:
|
|
46
|
+
for k in keys:
|
|
47
|
+
v = cfg.get(k)
|
|
48
|
+
if isinstance(v, bool):
|
|
49
|
+
return v
|
|
50
|
+
return default
|
|
51
|
+
|
|
52
|
+
def parse_csv_list(key: str) -> list:
|
|
53
|
+
raw = cfg.get(key, "")
|
|
54
|
+
if not raw:
|
|
55
|
+
return []
|
|
56
|
+
if isinstance(raw, list):
|
|
57
|
+
return [str(x).strip() for x in raw if str(x).strip()]
|
|
58
|
+
return [s.strip() for s in str(raw).split(",") if s.strip()]
|
|
59
|
+
|
|
60
|
+
# --- Web Search (server tool) ---
|
|
61
|
+
is_web = self.window.controller.chat.remote_tools.enabled(model, "web_search")
|
|
62
|
+
if is_web:
|
|
63
|
+
ttype = cfg.get("remote_tools.anthropic.web_search.type", "web_search_20250305")
|
|
64
|
+
tname = "web_search"
|
|
65
|
+
tool_def: Dict[str, Any] = {
|
|
66
|
+
"type": ttype,
|
|
67
|
+
"name": tname,
|
|
68
|
+
}
|
|
69
|
+
max_uses = cfg.get("remote_tools.anthropic.web_search.max_uses")
|
|
70
|
+
if isinstance(max_uses, int) and max_uses > 0:
|
|
71
|
+
tool_def["max_uses"] = max_uses
|
|
72
|
+
allowed = parse_csv_list("remote_tools.anthropic.web_search.allowed_domains")
|
|
73
|
+
blocked = parse_csv_list("remote_tools.anthropic.web_search.blocked_domains")
|
|
74
|
+
if allowed:
|
|
75
|
+
tool_def["allowed_domains"] = allowed
|
|
76
|
+
elif blocked:
|
|
77
|
+
tool_def["blocked_domains"] = blocked
|
|
78
|
+
loc_city = cfg.get("remote_tools.anthropic.web_search.user_location.city")
|
|
79
|
+
loc_region = cfg.get("remote_tools.anthropic.web_search.user_location.region")
|
|
80
|
+
loc_country = cfg.get("remote_tools.anthropic.web_search.user_location.country")
|
|
81
|
+
loc_tz = cfg.get("remote_tools.anthropic.web_search.user_location.timezone")
|
|
82
|
+
if any([loc_city, loc_region, loc_country, loc_tz]):
|
|
83
|
+
tool_def["user_location"] = {
|
|
84
|
+
"type": "approximate",
|
|
85
|
+
"city": str(loc_city) if loc_city else None,
|
|
86
|
+
"region": str(loc_region) if loc_region else None,
|
|
87
|
+
"country": str(loc_country) if loc_country else None,
|
|
88
|
+
"timezone": str(loc_tz) if loc_tz else None,
|
|
89
|
+
}
|
|
90
|
+
tool_def["user_location"] = {k: v for k, v in tool_def["user_location"].items() if v is not None}
|
|
91
|
+
tools.append(tool_def)
|
|
92
|
+
|
|
93
|
+
# --- Code Execution (server tool) ---
|
|
94
|
+
is_code_exec = cfg_bool("remote_tools.anthropic.code_execution", default=False)
|
|
95
|
+
if is_code_exec:
|
|
96
|
+
tools.append({
|
|
97
|
+
"type": "code_execution_20250825",
|
|
98
|
+
"name": "code_execution",
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
# --- Web Fetch (server tool) ---
|
|
102
|
+
is_web_fetch = cfg_bool("remote_tools.anthropic.web_fetch", default=False)
|
|
103
|
+
if is_web_fetch:
|
|
104
|
+
fetch_def: Dict[str, Any] = {
|
|
105
|
+
"type": "web_fetch_20250910",
|
|
106
|
+
"name": "web_fetch",
|
|
107
|
+
}
|
|
108
|
+
max_uses = cfg.get("remote_tools.anthropic.web_fetch.max_uses")
|
|
109
|
+
if isinstance(max_uses, int) and max_uses > 0:
|
|
110
|
+
fetch_def["max_uses"] = max_uses
|
|
111
|
+
allowed = parse_csv_list("remote_tools.anthropic.web_fetch.allowed_domains")
|
|
112
|
+
blocked = parse_csv_list("remote_tools.anthropic.web_fetch.blocked_domains")
|
|
113
|
+
if allowed:
|
|
114
|
+
fetch_def["allowed_domains"] = allowed
|
|
115
|
+
elif blocked:
|
|
116
|
+
fetch_def["blocked_domains"] = blocked
|
|
117
|
+
citations_enabled = cfg_bool("remote_tools.anthropic.web_fetch.citations.enabled", default=True)
|
|
118
|
+
if citations_enabled:
|
|
119
|
+
fetch_def["citations"] = {"enabled": True}
|
|
120
|
+
max_content_tokens = cfg.get("remote_tools.anthropic.web_fetch.max_content_tokens")
|
|
121
|
+
if isinstance(max_content_tokens, int) and max_content_tokens > 0:
|
|
122
|
+
fetch_def["max_content_tokens"] = max_content_tokens
|
|
123
|
+
tools.append(fetch_def)
|
|
124
|
+
|
|
125
|
+
# --- Tool Search (server tool) ---
|
|
126
|
+
"""
|
|
127
|
+
is_tool_search = cfg_bool("remote_tools.anthropic.tool_search", default=False)
|
|
128
|
+
if is_tool_search:
|
|
129
|
+
variant = (cfg.get("remote_tools.anthropic.tool_search.variant")
|
|
130
|
+
or cfg.get("remote_tools.tool_search.variant") or "regex")
|
|
131
|
+
# accept full type as well
|
|
132
|
+
raw_type = str(cfg.get("remote_tools.anthropic.tool_search.type")
|
|
133
|
+
or cfg.get("remote_tools.tool_search.type") or "").strip()
|
|
134
|
+
if raw_type.startswith("tool_search_tool_"):
|
|
135
|
+
ttype = raw_type
|
|
136
|
+
tname = "tool_search_tool_regex" if "regex" in raw_type else "tool_search_tool_bm25"
|
|
137
|
+
else:
|
|
138
|
+
if str(variant).lower() == "bm25":
|
|
139
|
+
ttype = "tool_search_tool_bm25_20251119"
|
|
140
|
+
tname = "tool_search_tool_bm25"
|
|
141
|
+
else:
|
|
142
|
+
ttype = "tool_search_tool_regex_20251119"
|
|
143
|
+
tname = "tool_search_tool_regex"
|
|
144
|
+
tools.append({
|
|
145
|
+
"type": ttype,
|
|
146
|
+
"name": tname,
|
|
147
|
+
})
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
# --- MCP toolset (server-side tool catalog from MCP servers) ---
|
|
151
|
+
is_mcp = cfg_bool("remote_tools.anthropic.mcp", default=False)
|
|
152
|
+
if is_mcp:
|
|
153
|
+
raw_tools = cfg.get("remote_tools.anthropic.mcp.tools")
|
|
154
|
+
if raw_tools:
|
|
155
|
+
try:
|
|
156
|
+
if isinstance(raw_tools, (list, dict)):
|
|
157
|
+
mcp_tools = raw_tools
|
|
158
|
+
else:
|
|
159
|
+
mcp_tools = json.loads(raw_tools)
|
|
160
|
+
# ensure list
|
|
161
|
+
if isinstance(mcp_tools, dict):
|
|
162
|
+
mcp_tools = [mcp_tools]
|
|
163
|
+
for t in mcp_tools:
|
|
164
|
+
if isinstance(t, dict):
|
|
165
|
+
# default type if not set
|
|
166
|
+
if "type" not in t:
|
|
167
|
+
t["type"] = "mcp_toolset"
|
|
168
|
+
tools.append(t)
|
|
169
|
+
except Exception:
|
|
170
|
+
pass # ignore invalid JSON to avoid breaking existing flows
|
|
171
|
+
|
|
172
|
+
return tools
|
|
@@ -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:
|
|
9
|
+
# Updated Date: 2026.01.05 20:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import io
|
|
@@ -28,6 +28,18 @@ def process_anthropic_chunk(ctx, core, state, chunk) -> Optional[str]:
|
|
|
28
28
|
state.usage_vendor = "anthropic"
|
|
29
29
|
etype = str(getattr(chunk, "type", "") or "")
|
|
30
30
|
response: Optional[str] = None
|
|
31
|
+
is_computer_call = False
|
|
32
|
+
|
|
33
|
+
# Computer Use: translate Anthropic 'computer' tool_use stream into plugin calls
|
|
34
|
+
try:
|
|
35
|
+
tool_calls, has_calls = core.api.anthropic.computer.handle_stream_chunk(ctx, chunk, state.tool_calls)
|
|
36
|
+
state.tool_calls = tool_calls
|
|
37
|
+
if has_calls:
|
|
38
|
+
is_computer_call = True
|
|
39
|
+
state.force_func_call = True
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
|
|
31
43
|
|
|
32
44
|
# --- Top-level delta objects (when SDK yields deltas directly) ---
|
|
33
45
|
if etype == "text_delta":
|
|
@@ -37,7 +49,7 @@ def process_anthropic_chunk(ctx, core, state, chunk) -> Optional[str]:
|
|
|
37
49
|
if etype == "thinking_delta":
|
|
38
50
|
return None
|
|
39
51
|
|
|
40
|
-
if etype == "input_json_delta":
|
|
52
|
+
if etype == "input_json_delta" and not is_computer_call:
|
|
41
53
|
pj = getattr(chunk, "partial_json", "") or ""
|
|
42
54
|
buf = state.fn_args_buffers.get("__anthropic_last__")
|
|
43
55
|
if buf is None:
|
|
@@ -64,18 +76,20 @@ def process_anthropic_chunk(ctx, core, state, chunk) -> Optional[str]:
|
|
|
64
76
|
pass
|
|
65
77
|
return None
|
|
66
78
|
|
|
67
|
-
if etype == "content_block_start":
|
|
79
|
+
if etype == "content_block_start" and not is_computer_call:
|
|
68
80
|
try:
|
|
69
81
|
cb = getattr(chunk, "content_block", None)
|
|
70
82
|
if cb and getattr(cb, "type", "") == "tool_use":
|
|
71
83
|
idx = getattr(chunk, "index", 0) or 0
|
|
72
84
|
tid = getattr(cb, "id", "") or ""
|
|
73
85
|
name = getattr(cb, "name", "") or ""
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
86
|
+
# Skip generic function-call for Anthropic Computer Use; the adapter will emit computer_call items.
|
|
87
|
+
if name not in {"computer", "computer.use", "anthropic/computer", "computer_use", "computer-use"}:
|
|
88
|
+
state.tool_calls.append({
|
|
89
|
+
"id": tid,
|
|
90
|
+
"type": "function",
|
|
91
|
+
"function": {"name": name, "arguments": ""}
|
|
92
|
+
})
|
|
79
93
|
state.fn_args_buffers[str(idx)] = io.StringIO()
|
|
80
94
|
state.fn_args_buffers["__anthropic_last__"] = state.fn_args_buffers[str(idx)]
|
|
81
95
|
except Exception:
|
|
@@ -97,7 +111,7 @@ def process_anthropic_chunk(ctx, core, state, chunk) -> Optional[str]:
|
|
|
97
111
|
|
|
98
112
|
return None
|
|
99
113
|
|
|
100
|
-
if etype == "content_block_delta":
|
|
114
|
+
if etype == "content_block_delta" and not is_computer_call:
|
|
101
115
|
try:
|
|
102
116
|
delta = getattr(chunk, "delta", None)
|
|
103
117
|
if not delta:
|
|
@@ -125,7 +139,7 @@ def process_anthropic_chunk(ctx, core, state, chunk) -> Optional[str]:
|
|
|
125
139
|
pass
|
|
126
140
|
return response
|
|
127
141
|
|
|
128
|
-
if etype == "content_block_stop":
|
|
142
|
+
if etype == "content_block_stop" and not is_computer_call:
|
|
129
143
|
try:
|
|
130
144
|
idx = str(getattr(chunk, "index", 0) or 0)
|
|
131
145
|
buf = state.fn_args_buffers.pop(idx, 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:
|
|
9
|
+
# Updated Date: 2026.01.05 20:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import json
|
|
@@ -152,78 +152,16 @@ class Tools:
|
|
|
152
152
|
if not params.get("type"):
|
|
153
153
|
params["type"] = "object"
|
|
154
154
|
|
|
155
|
-
|
|
155
|
+
# pass through tool as client tool
|
|
156
|
+
tool_def = {
|
|
156
157
|
"name": name,
|
|
157
158
|
"description": desc,
|
|
158
159
|
"input_schema": params or {"type": "object"},
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
return tools
|
|
162
|
-
|
|
163
|
-
def build_remote_tools(self, model: ModelItem = None) -> List[dict]:
|
|
164
|
-
"""
|
|
165
|
-
Build Anthropic server tools (remote tools) based on config flags.
|
|
166
|
-
Currently supports: Web Search tool.
|
|
167
|
-
|
|
168
|
-
Returns a list of tool dicts to be appended to 'tools' in messages.create.
|
|
169
|
-
|
|
170
|
-
:param model: ModelItem
|
|
171
|
-
:return: List of remote tool dicts
|
|
172
|
-
"""
|
|
173
|
-
cfg = self.window.core.config
|
|
174
|
-
tools: List[dict] = []
|
|
175
|
-
|
|
176
|
-
# sonnet-3.5 is not supported
|
|
177
|
-
if model and model.id and model.id.startswith("claude-3-5"):
|
|
178
|
-
return tools
|
|
179
|
-
|
|
180
|
-
is_web = self.window.controller.chat.remote_tools.enabled(model, "web_search") # get global config
|
|
181
|
-
|
|
182
|
-
# Web Search tool
|
|
183
|
-
if is_web:
|
|
184
|
-
ttype = cfg.get("remote_tools.anthropic.web_search.type", "web_search_20250305") # stable as of docs
|
|
185
|
-
tname = "web_search"
|
|
186
|
-
|
|
187
|
-
tool_def: Dict[str, Any] = {
|
|
188
|
-
"type": ttype,
|
|
189
|
-
"name": tname,
|
|
190
160
|
}
|
|
191
161
|
|
|
192
|
-
#
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
tool_def["max_uses"] = max_uses
|
|
196
|
-
|
|
197
|
-
def parse_csv_list(key: str) -> list:
|
|
198
|
-
raw = cfg.get(key, "")
|
|
199
|
-
if not raw:
|
|
200
|
-
return []
|
|
201
|
-
if isinstance(raw, list):
|
|
202
|
-
return [str(x).strip() for x in raw if str(x).strip()]
|
|
203
|
-
return [s.strip() for s in str(raw).split(",") if s.strip()]
|
|
204
|
-
|
|
205
|
-
allowed = parse_csv_list("remote_tools.anthropic.web_search.allowed_domains")
|
|
206
|
-
blocked = parse_csv_list("remote_tools.anthropic.web_search.blocked_domains")
|
|
207
|
-
if allowed:
|
|
208
|
-
tool_def["allowed_domains"] = allowed
|
|
209
|
-
elif blocked:
|
|
210
|
-
tool_def["blocked_domains"] = blocked
|
|
211
|
-
|
|
212
|
-
# Location (approximate)
|
|
213
|
-
loc_city = cfg.get("remote_tools.anthropic.web_search.user_location.city")
|
|
214
|
-
loc_region = cfg.get("remote_tools.anthropic.web_search.user_location.region")
|
|
215
|
-
loc_country = cfg.get("remote_tools.anthropic.web_search.user_location.country")
|
|
216
|
-
loc_tz = cfg.get("remote_tools.anthropic.web_search.user_location.timezone")
|
|
217
|
-
if any([loc_city, loc_region, loc_country, loc_tz]):
|
|
218
|
-
tool_def["user_location"] = {
|
|
219
|
-
"type": "approximate",
|
|
220
|
-
"city": str(loc_city) if loc_city else None,
|
|
221
|
-
"region": str(loc_region) if loc_region else None,
|
|
222
|
-
"country": str(loc_country) if loc_country else None,
|
|
223
|
-
"timezone": str(loc_tz) if loc_tz else None,
|
|
224
|
-
}
|
|
225
|
-
# remove None fields
|
|
226
|
-
tool_def["user_location"] = {k: v for k, v in tool_def["user_location"].items() if v is not None}
|
|
162
|
+
# optional: allow defer_loading for tool search when configured per-tool (kept compatible)
|
|
163
|
+
if isinstance(fn, dict) and fn.get("defer_loading") is True:
|
|
164
|
+
tool_def["defer_loading"] = True
|
|
227
165
|
|
|
228
166
|
tools.append(tool_def)
|
|
229
167
|
|
|
@@ -231,28 +169,45 @@ class Tools:
|
|
|
231
169
|
|
|
232
170
|
def merge_tools_dedup(self, primary: List[dict], secondary: List[dict]) -> List[dict]:
|
|
233
171
|
"""
|
|
234
|
-
Remove duplicate tools
|
|
172
|
+
Remove duplicate tools, preserving order:
|
|
235
173
|
|
|
236
174
|
- First from primary list
|
|
237
|
-
- Then from secondary list if
|
|
175
|
+
- Then from secondary list if not already present
|
|
176
|
+
|
|
177
|
+
Dedup rules:
|
|
178
|
+
* Tools with a 'name' are deduped by name.
|
|
179
|
+
* MCP toolsets (type == 'mcp_toolset') are deduped by (type, mcp_server_name).
|
|
180
|
+
* Tools without a 'name' use (type) as a fallback key.
|
|
238
181
|
|
|
239
182
|
:param primary: Primary list of tool dicts
|
|
240
183
|
:param secondary: Secondary list of tool dicts
|
|
241
184
|
:return: Merged list of tool dicts without duplicates
|
|
242
185
|
"""
|
|
186
|
+
def key_for(t: dict) -> str:
|
|
187
|
+
name = t.get("name")
|
|
188
|
+
if name:
|
|
189
|
+
return f"name::{name}"
|
|
190
|
+
ttype = t.get("type")
|
|
191
|
+
if ttype == "mcp_toolset":
|
|
192
|
+
return f"mcp::{t.get('mcp_server_name', '')}"
|
|
193
|
+
return f"type::{ttype}"
|
|
194
|
+
|
|
243
195
|
result: List[dict] = []
|
|
244
196
|
seen = set()
|
|
197
|
+
|
|
245
198
|
for t in primary or []:
|
|
246
|
-
|
|
247
|
-
if
|
|
248
|
-
seen.add(
|
|
199
|
+
k = key_for(t)
|
|
200
|
+
if k not in seen:
|
|
201
|
+
seen.add(k)
|
|
249
202
|
result.append(t)
|
|
203
|
+
|
|
250
204
|
for t in secondary or []:
|
|
251
|
-
|
|
252
|
-
if
|
|
205
|
+
k = key_for(t)
|
|
206
|
+
if k in seen:
|
|
253
207
|
continue
|
|
254
|
-
seen.add(
|
|
208
|
+
seen.add(k)
|
|
255
209
|
result.append(t)
|
|
210
|
+
|
|
256
211
|
return result
|
|
257
212
|
|
|
258
213
|
def get_all_tools(self, model: ModelItem, functions: list) -> List[dict]:
|
|
@@ -264,5 +219,5 @@ class Tools:
|
|
|
264
219
|
:return: Combined list of tool dicts
|
|
265
220
|
"""
|
|
266
221
|
base_tools = self.prepare(model, functions)
|
|
267
|
-
remote_tools = self.build_remote_tools(model)
|
|
222
|
+
remote_tools = self.window.core.api.anthropic.remote_tools.build_remote_tools(model)
|
|
268
223
|
return self.merge_tools_dedup(base_tools, remote_tools)
|
|
@@ -0,0 +1,30 @@
|
|
|
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: 2026.01.05 20:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def as_int(val: Any) -> Optional[int]:
|
|
16
|
+
"""
|
|
17
|
+
Coerce to int if possible, else None.
|
|
18
|
+
|
|
19
|
+
:param val: Input value
|
|
20
|
+
:return: int or None
|
|
21
|
+
"""
|
|
22
|
+
if val is None:
|
|
23
|
+
return None
|
|
24
|
+
try:
|
|
25
|
+
return int(val)
|
|
26
|
+
except Exception:
|
|
27
|
+
try:
|
|
28
|
+
return int(float(val))
|
|
29
|
+
except Exception:
|
|
30
|
+
return 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: 2026.01.
|
|
9
|
+
# Updated Date: 2026.01.05 20:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import os
|
|
@@ -121,12 +121,8 @@ class Chat:
|
|
|
121
121
|
|
|
122
122
|
# Enable Computer Use tool in computer mode (use the official Tool/ComputerUse object)
|
|
123
123
|
if mode == MODE_COMPUTER or (model and isinstance(model.id, str) and "computer-use" in model.id.lower()):
|
|
124
|
-
|
|
125
|
-
tools = [
|
|
126
|
-
computer_use=gtypes.ComputerUse(
|
|
127
|
-
environment=comp_env,
|
|
128
|
-
)
|
|
129
|
-
)] # reset tools to only Computer Use (multiple tools not supported together)
|
|
124
|
+
tool = self.window.core.api.google.computer.get_tool()
|
|
125
|
+
tools = [tool] # reset tools to only Computer Use (multiple tools not supported together)
|
|
130
126
|
|
|
131
127
|
# Some models cannot use tools; keep behavior for image-only models
|
|
132
128
|
if model and isinstance(model.id, str) and "-image" in model.id:
|
|
@@ -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: 2026.01.
|
|
9
|
+
# Updated Date: 2026.01.05 20:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import base64
|
|
@@ -0,0 +1,185 @@
|
|
|
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: 2026.01.05 20:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def safe_get(obj: Any, path: str) -> Any:
|
|
16
|
+
"""
|
|
17
|
+
Dot-path getter for dicts and objects.
|
|
18
|
+
|
|
19
|
+
:param obj: Source object or dict
|
|
20
|
+
:param path: Dot-separated path, e.g. 'a.b.0.c'
|
|
21
|
+
:return: Value at path or None
|
|
22
|
+
"""
|
|
23
|
+
cur = obj
|
|
24
|
+
for seg in path.split("."):
|
|
25
|
+
if cur is None:
|
|
26
|
+
return None
|
|
27
|
+
if isinstance(cur, dict):
|
|
28
|
+
cur = cur.get(seg)
|
|
29
|
+
else:
|
|
30
|
+
if seg.isdigit() and isinstance(cur, (list, tuple)):
|
|
31
|
+
idx = int(seg)
|
|
32
|
+
if 0 <= idx < len(cur):
|
|
33
|
+
cur = cur[idx]
|
|
34
|
+
else:
|
|
35
|
+
return None
|
|
36
|
+
else:
|
|
37
|
+
cur = getattr(cur, seg, None)
|
|
38
|
+
return cur
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def as_int(val: Any) -> Optional[int]:
|
|
42
|
+
"""
|
|
43
|
+
Coerce to int if possible, else None.
|
|
44
|
+
|
|
45
|
+
:param val: Input value
|
|
46
|
+
:return: int or None
|
|
47
|
+
"""
|
|
48
|
+
if val is None:
|
|
49
|
+
return None
|
|
50
|
+
try:
|
|
51
|
+
return int(val)
|
|
52
|
+
except Exception:
|
|
53
|
+
try:
|
|
54
|
+
return int(float(val))
|
|
55
|
+
except Exception:
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
def capture_google_usage(state, um_obj: Any):
|
|
59
|
+
"""
|
|
60
|
+
Extract usage for Google python-genai; prefer total - prompt to include reasoning.
|
|
61
|
+
|
|
62
|
+
:param state: Chat state
|
|
63
|
+
:param um_obj: Usage metadata object/dict
|
|
64
|
+
"""
|
|
65
|
+
if not um_obj:
|
|
66
|
+
return
|
|
67
|
+
state.usage_vendor = "google"
|
|
68
|
+
prompt = (
|
|
69
|
+
as_int(safe_get(um_obj, "prompt_token_count")) or
|
|
70
|
+
as_int(safe_get(um_obj, "prompt_tokens")) or
|
|
71
|
+
as_int(safe_get(um_obj, "input_tokens"))
|
|
72
|
+
)
|
|
73
|
+
total = (
|
|
74
|
+
as_int(safe_get(um_obj, "total_token_count")) or
|
|
75
|
+
as_int(safe_get(um_obj, "total_tokens"))
|
|
76
|
+
)
|
|
77
|
+
candidates = (
|
|
78
|
+
as_int(safe_get(um_obj, "candidates_token_count")) or
|
|
79
|
+
as_int(safe_get(um_obj, "output_tokens"))
|
|
80
|
+
)
|
|
81
|
+
reasoning = (
|
|
82
|
+
as_int(safe_get(um_obj, "candidates_reasoning_token_count")) or
|
|
83
|
+
as_int(safe_get(um_obj, "reasoning_tokens")) or 0
|
|
84
|
+
)
|
|
85
|
+
if total is not None and prompt is not None:
|
|
86
|
+
out_total = max(0, total - prompt)
|
|
87
|
+
else:
|
|
88
|
+
out_total = candidates
|
|
89
|
+
state.usage_payload = {"in": prompt, "out": out_total, "reasoning": reasoning or 0, "total": total}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def collect_google_citations(ctx, state, chunk: Any):
|
|
93
|
+
"""
|
|
94
|
+
Collect web citations (URLs) from Google GenAI stream.
|
|
95
|
+
|
|
96
|
+
:param ctx: Chat context
|
|
97
|
+
:param state: Chat state
|
|
98
|
+
:param chunk: Incoming streaming chunk
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
cands = getattr(chunk, "candidates", None) or []
|
|
102
|
+
except Exception:
|
|
103
|
+
cands = []
|
|
104
|
+
|
|
105
|
+
if not isinstance(state.citations, list):
|
|
106
|
+
state.citations = []
|
|
107
|
+
|
|
108
|
+
def _add_url(url: Optional[str]):
|
|
109
|
+
if not url or not isinstance(url, str):
|
|
110
|
+
return
|
|
111
|
+
url = url.strip()
|
|
112
|
+
if not (url.startswith("http://") or url.startswith("https://")):
|
|
113
|
+
return
|
|
114
|
+
if ctx.urls is None:
|
|
115
|
+
ctx.urls = []
|
|
116
|
+
if url not in state.citations:
|
|
117
|
+
state.citations.append(url)
|
|
118
|
+
if url not in ctx.urls:
|
|
119
|
+
ctx.urls.append(url)
|
|
120
|
+
|
|
121
|
+
for cand in cands:
|
|
122
|
+
gm = safe_get(cand, "grounding_metadata") or safe_get(cand, "groundingMetadata")
|
|
123
|
+
if gm:
|
|
124
|
+
atts = safe_get(gm, "grounding_attributions") or safe_get(gm, "groundingAttributions") or []
|
|
125
|
+
try:
|
|
126
|
+
for att in atts or []:
|
|
127
|
+
for path in (
|
|
128
|
+
"web.uri",
|
|
129
|
+
"web.url",
|
|
130
|
+
"source.web.uri",
|
|
131
|
+
"source.web.url",
|
|
132
|
+
"source.uri",
|
|
133
|
+
"source.url",
|
|
134
|
+
"uri",
|
|
135
|
+
"url",
|
|
136
|
+
):
|
|
137
|
+
_add_url(safe_get(att, path))
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
for path in (
|
|
141
|
+
"search_entry_point.uri",
|
|
142
|
+
"search_entry_point.url",
|
|
143
|
+
"searchEntryPoint.uri",
|
|
144
|
+
"searchEntryPoint.url",
|
|
145
|
+
"search_entry_point.rendered_content_uri",
|
|
146
|
+
"searchEntryPoint.rendered_content_uri",
|
|
147
|
+
):
|
|
148
|
+
_add_url(safe_get(gm, path))
|
|
149
|
+
|
|
150
|
+
cm = safe_get(cand, "citation_metadata") or safe_get(cand, "citationMetadata")
|
|
151
|
+
if cm:
|
|
152
|
+
cit_arrays = (
|
|
153
|
+
safe_get(cm, "citation_sources") or
|
|
154
|
+
safe_get(cm, "citationSources") or
|
|
155
|
+
safe_get(cm, "citations") or []
|
|
156
|
+
)
|
|
157
|
+
try:
|
|
158
|
+
for cit in cit_arrays or []:
|
|
159
|
+
for path in ("uri", "url", "source.uri", "source.url", "web.uri", "web.url"):
|
|
160
|
+
_add_url(safe_get(cit, path))
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
parts = safe_get(cand, "content.parts") or []
|
|
166
|
+
for p in parts:
|
|
167
|
+
pcm = safe_get(p, "citation_metadata") or safe_get(p, "citationMetadata")
|
|
168
|
+
if pcm:
|
|
169
|
+
arr = (
|
|
170
|
+
safe_get(pcm, "citation_sources") or
|
|
171
|
+
safe_get(pcm, "citationSources") or
|
|
172
|
+
safe_get(pcm, "citations") or []
|
|
173
|
+
)
|
|
174
|
+
for cit in arr or []:
|
|
175
|
+
for path in ("uri", "url", "source.uri", "source.url", "web.uri", "web.url"):
|
|
176
|
+
_add_url(safe_get(cit, path))
|
|
177
|
+
gpa = safe_get(p, "grounding_attributions") or safe_get(p, "groundingAttributions") or []
|
|
178
|
+
for att in gpa or []:
|
|
179
|
+
for path in ("web.uri", "web.url", "source.web.uri", "source.web.url", "uri", "url"):
|
|
180
|
+
_add_url(safe_get(att, path))
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
if state.citations and (ctx.urls is None or not ctx.urls):
|
|
185
|
+
ctx.urls = list(state.citations)
|
|
File without changes
|
|
@@ -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:
|
|
9
|
+
# Updated Date: 2026.01.05 20:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from typing import Optional
|
|
File without changes
|