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.
Files changed (57) hide show
  1. pygpt_net/CHANGELOG.txt +6 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/chat/remote_tools.py +3 -9
  4. pygpt_net/controller/chat/stream.py +2 -2
  5. pygpt_net/controller/chat/{handler/worker.py → stream_worker.py} +13 -35
  6. pygpt_net/core/debug/models.py +2 -2
  7. pygpt_net/data/config/config.json +14 -4
  8. pygpt_net/data/config/models.json +192 -4
  9. pygpt_net/data/config/settings.json +125 -35
  10. pygpt_net/data/locale/locale.de.ini +2 -0
  11. pygpt_net/data/locale/locale.en.ini +32 -8
  12. pygpt_net/data/locale/locale.es.ini +2 -0
  13. pygpt_net/data/locale/locale.fr.ini +2 -0
  14. pygpt_net/data/locale/locale.it.ini +2 -0
  15. pygpt_net/data/locale/locale.pl.ini +3 -1
  16. pygpt_net/data/locale/locale.uk.ini +2 -0
  17. pygpt_net/data/locale/locale.zh.ini +2 -0
  18. pygpt_net/plugin/cmd_mouse_control/worker.py +2 -1
  19. pygpt_net/plugin/cmd_mouse_control/worker_sandbox.py +2 -1
  20. pygpt_net/provider/api/anthropic/__init__.py +8 -3
  21. pygpt_net/provider/api/anthropic/chat.py +259 -11
  22. pygpt_net/provider/api/anthropic/computer.py +844 -0
  23. pygpt_net/provider/api/anthropic/remote_tools.py +172 -0
  24. pygpt_net/{controller/chat/handler/anthropic_stream.py → provider/api/anthropic/stream.py} +24 -10
  25. pygpt_net/provider/api/anthropic/tools.py +32 -77
  26. pygpt_net/provider/api/anthropic/utils.py +30 -0
  27. pygpt_net/provider/api/google/chat.py +3 -7
  28. pygpt_net/{controller/chat/handler/google_stream.py → provider/api/google/stream.py} +1 -1
  29. pygpt_net/provider/api/google/utils.py +185 -0
  30. pygpt_net/{controller/chat/handler → provider/api/langchain}/__init__.py +0 -0
  31. pygpt_net/{controller/chat/handler/langchain_stream.py → provider/api/langchain/stream.py} +1 -1
  32. pygpt_net/provider/api/llama_index/__init__.py +0 -0
  33. pygpt_net/{controller/chat/handler/llamaindex_stream.py → provider/api/llama_index/stream.py} +1 -1
  34. pygpt_net/provider/api/openai/image.py +2 -2
  35. pygpt_net/{controller/chat/handler/openai_stream.py → provider/api/openai/stream.py} +1 -1
  36. pygpt_net/provider/api/openai/utils.py +69 -3
  37. pygpt_net/provider/api/x_ai/__init__.py +109 -10
  38. pygpt_net/provider/api/x_ai/chat.py +0 -0
  39. pygpt_net/provider/api/x_ai/image.py +149 -47
  40. pygpt_net/provider/api/x_ai/{remote.py → remote_tools.py} +165 -70
  41. pygpt_net/provider/api/x_ai/responses.py +507 -0
  42. pygpt_net/{controller/chat/handler/xai_stream.py → provider/api/x_ai/stream.py} +12 -1
  43. pygpt_net/provider/api/x_ai/tools.py +59 -8
  44. pygpt_net/{controller/chat/handler → provider/api/x_ai}/utils.py +1 -2
  45. pygpt_net/provider/api/x_ai/vision.py +1 -4
  46. pygpt_net/provider/core/config/patch.py +22 -1
  47. pygpt_net/provider/core/model/patch.py +26 -1
  48. pygpt_net/tools/image_viewer/ui/dialogs.py +3 -2
  49. pygpt_net/tools/text_editor/ui/dialogs.py +3 -2
  50. pygpt_net/tools/text_editor/ui/widgets.py +0 -0
  51. pygpt_net/ui/widget/dialog/base.py +16 -5
  52. pygpt_net/ui/widget/textarea/editor.py +0 -0
  53. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.7.dist-info}/METADATA +8 -2
  54. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.7.dist-info}/RECORD +54 -48
  55. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.7.dist-info}/LICENSE +0 -0
  56. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.7.dist-info}/WHEEL +0 -0
  57. {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: 2025.09.05 00:00:00 #
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
- state.tool_calls.append({
75
- "id": tid,
76
- "type": "function",
77
- "function": {"name": name, "arguments": ""}
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: 2025.09.17 05:00:00 #
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
- tools.append({
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
- # Optional params
193
- max_uses = cfg.get("remote_tools.anthropic.web_search.max_uses")
194
- if isinstance(max_uses, int) and max_uses > 0:
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 by name, preserving order:
172
+ Remove duplicate tools, preserving order:
235
173
 
236
174
  - First from primary list
237
- - Then from secondary list if name not already present
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
- n = t.get("name")
247
- if n and n not in seen:
248
- seen.add(n)
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
- n = t.get("name")
252
- if not n or n in seen:
205
+ k = key_for(t)
206
+ if k in seen:
253
207
  continue
254
- seen.add(n)
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.03 17:00:00 #
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
- comp_env = gtypes.Environment.ENVIRONMENT_BROWSER
125
- tools = [gtypes.Tool(
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.03 02:10:00 #
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)
@@ -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 00:00:00 #
9
+ # Updated Date: 2026.01.05 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Optional
File without changes