pygpt-net 2.7.6__py3-none-any.whl → 2.7.8__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 (120) hide show
  1. pygpt_net/CHANGELOG.txt +13 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +5 -1
  4. pygpt_net/controller/assistant/batch.py +2 -2
  5. pygpt_net/controller/assistant/files.py +7 -6
  6. pygpt_net/controller/assistant/threads.py +0 -0
  7. pygpt_net/controller/chat/command.py +0 -0
  8. pygpt_net/controller/chat/remote_tools.py +3 -9
  9. pygpt_net/controller/chat/stream.py +2 -2
  10. pygpt_net/controller/chat/{handler/worker.py → stream_worker.py} +13 -35
  11. pygpt_net/controller/dialogs/confirm.py +35 -58
  12. pygpt_net/controller/lang/mapping.py +9 -9
  13. pygpt_net/controller/remote_store/{google/batch.py → batch.py} +209 -252
  14. pygpt_net/controller/remote_store/remote_store.py +982 -13
  15. pygpt_net/core/command/command.py +0 -0
  16. pygpt_net/core/db/viewer.py +1 -1
  17. pygpt_net/core/debug/models.py +2 -2
  18. pygpt_net/core/realtime/worker.py +3 -1
  19. pygpt_net/{controller/remote_store/google → core/remote_store/anthropic}/__init__.py +0 -1
  20. pygpt_net/core/remote_store/anthropic/files.py +211 -0
  21. pygpt_net/core/remote_store/anthropic/store.py +208 -0
  22. pygpt_net/core/remote_store/openai/store.py +5 -4
  23. pygpt_net/core/remote_store/remote_store.py +5 -1
  24. pygpt_net/{controller/remote_store/openai → core/remote_store/xai}/__init__.py +0 -1
  25. pygpt_net/core/remote_store/xai/files.py +225 -0
  26. pygpt_net/core/remote_store/xai/store.py +219 -0
  27. pygpt_net/data/config/config.json +18 -5
  28. pygpt_net/data/config/models.json +193 -4
  29. pygpt_net/data/config/settings.json +179 -36
  30. pygpt_net/data/icons/folder_eye.svg +1 -0
  31. pygpt_net/data/icons/folder_eye_filled.svg +1 -0
  32. pygpt_net/data/icons/folder_open.svg +1 -0
  33. pygpt_net/data/icons/folder_open_filled.svg +1 -0
  34. pygpt_net/data/locale/locale.de.ini +6 -3
  35. pygpt_net/data/locale/locale.en.ini +46 -12
  36. pygpt_net/data/locale/locale.es.ini +6 -3
  37. pygpt_net/data/locale/locale.fr.ini +6 -3
  38. pygpt_net/data/locale/locale.it.ini +6 -3
  39. pygpt_net/data/locale/locale.pl.ini +7 -4
  40. pygpt_net/data/locale/locale.uk.ini +6 -3
  41. pygpt_net/data/locale/locale.zh.ini +6 -3
  42. pygpt_net/icons.qrc +4 -0
  43. pygpt_net/icons_rc.py +282 -138
  44. pygpt_net/plugin/cmd_mouse_control/worker.py +2 -1
  45. pygpt_net/plugin/cmd_mouse_control/worker_sandbox.py +2 -1
  46. pygpt_net/provider/api/anthropic/__init__.py +10 -3
  47. pygpt_net/provider/api/anthropic/chat.py +342 -11
  48. pygpt_net/provider/api/anthropic/computer.py +844 -0
  49. pygpt_net/provider/api/anthropic/remote_tools.py +172 -0
  50. pygpt_net/provider/api/anthropic/store.py +307 -0
  51. pygpt_net/{controller/chat/handler/anthropic_stream.py → provider/api/anthropic/stream.py} +99 -10
  52. pygpt_net/provider/api/anthropic/tools.py +32 -77
  53. pygpt_net/provider/api/anthropic/utils.py +30 -0
  54. pygpt_net/{controller/chat/handler → provider/api/anthropic/worker}/__init__.py +0 -0
  55. pygpt_net/provider/api/anthropic/worker/importer.py +278 -0
  56. pygpt_net/provider/api/google/chat.py +62 -9
  57. pygpt_net/provider/api/google/store.py +124 -3
  58. pygpt_net/{controller/chat/handler/google_stream.py → provider/api/google/stream.py} +92 -25
  59. pygpt_net/provider/api/google/utils.py +185 -0
  60. pygpt_net/provider/api/google/worker/importer.py +16 -28
  61. pygpt_net/provider/api/langchain/__init__.py +0 -0
  62. pygpt_net/{controller/chat/handler/langchain_stream.py → provider/api/langchain/stream.py} +1 -1
  63. pygpt_net/provider/api/llama_index/__init__.py +0 -0
  64. pygpt_net/{controller/chat/handler/llamaindex_stream.py → provider/api/llama_index/stream.py} +1 -1
  65. pygpt_net/provider/api/openai/assistants.py +2 -2
  66. pygpt_net/provider/api/openai/image.py +2 -2
  67. pygpt_net/provider/api/openai/store.py +4 -1
  68. pygpt_net/{controller/chat/handler/openai_stream.py → provider/api/openai/stream.py} +1 -1
  69. pygpt_net/provider/api/openai/utils.py +69 -3
  70. pygpt_net/provider/api/openai/worker/importer.py +19 -61
  71. pygpt_net/provider/api/openai/worker/importer_assistants.py +230 -0
  72. pygpt_net/provider/api/x_ai/__init__.py +138 -15
  73. pygpt_net/provider/api/x_ai/audio.py +43 -11
  74. pygpt_net/provider/api/x_ai/chat.py +92 -4
  75. pygpt_net/provider/api/x_ai/image.py +149 -47
  76. pygpt_net/provider/api/x_ai/realtime/__init__.py +12 -0
  77. pygpt_net/provider/api/x_ai/realtime/client.py +1825 -0
  78. pygpt_net/provider/api/x_ai/realtime/realtime.py +198 -0
  79. pygpt_net/provider/api/x_ai/{remote.py → remote_tools.py} +183 -70
  80. pygpt_net/provider/api/x_ai/responses.py +507 -0
  81. pygpt_net/provider/api/x_ai/store.py +610 -0
  82. pygpt_net/{controller/chat/handler/xai_stream.py → provider/api/x_ai/stream.py} +42 -10
  83. pygpt_net/provider/api/x_ai/tools.py +59 -8
  84. pygpt_net/{controller/chat/handler → provider/api/x_ai}/utils.py +1 -2
  85. pygpt_net/provider/api/x_ai/vision.py +1 -4
  86. pygpt_net/provider/api/x_ai/worker/importer.py +308 -0
  87. pygpt_net/provider/audio_input/xai_grok_voice.py +390 -0
  88. pygpt_net/provider/audio_output/xai_tts.py +325 -0
  89. pygpt_net/provider/core/config/patch.py +39 -3
  90. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +2 -2
  91. pygpt_net/provider/core/model/patch.py +39 -1
  92. pygpt_net/tools/image_viewer/tool.py +334 -34
  93. pygpt_net/tools/image_viewer/ui/dialogs.py +319 -22
  94. pygpt_net/tools/text_editor/ui/dialogs.py +3 -2
  95. pygpt_net/tools/text_editor/ui/widgets.py +0 -0
  96. pygpt_net/ui/dialog/assistant.py +1 -1
  97. pygpt_net/ui/dialog/plugins.py +13 -5
  98. pygpt_net/ui/dialog/remote_store.py +552 -0
  99. pygpt_net/ui/dialogs.py +3 -5
  100. pygpt_net/ui/layout/ctx/ctx_list.py +58 -7
  101. pygpt_net/ui/menu/tools.py +6 -13
  102. pygpt_net/ui/widget/dialog/base.py +16 -5
  103. pygpt_net/ui/widget/dialog/{remote_store_google.py → remote_store.py} +10 -10
  104. pygpt_net/ui/widget/element/button.py +4 -4
  105. pygpt_net/ui/widget/image/display.py +2 -2
  106. pygpt_net/ui/widget/lists/context.py +2 -2
  107. pygpt_net/ui/widget/textarea/editor.py +0 -0
  108. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.dist-info}/METADATA +15 -2
  109. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.dist-info}/RECORD +107 -89
  110. pygpt_net/controller/remote_store/google/store.py +0 -615
  111. pygpt_net/controller/remote_store/openai/batch.py +0 -524
  112. pygpt_net/controller/remote_store/openai/store.py +0 -699
  113. pygpt_net/ui/dialog/remote_store_google.py +0 -539
  114. pygpt_net/ui/dialog/remote_store_openai.py +0 -539
  115. pygpt_net/ui/widget/dialog/remote_store_openai.py +0 -56
  116. pygpt_net/ui/widget/lists/remote_store_google.py +0 -248
  117. pygpt_net/ui/widget/lists/remote_store_openai.py +0 -317
  118. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.dist-info}/LICENSE +0 -0
  119. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.dist-info}/WHEEL +0 -0
  120. {pygpt_net-2.7.6.dist-info → pygpt_net-2.7.8.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
@@ -0,0 +1,307 @@
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 os
13
+ import mimetypes
14
+ import time
15
+ from typing import Optional, List, Dict, Any
16
+
17
+ from .worker.importer import Importer
18
+
19
+
20
+ class Store:
21
+ """
22
+ Anthropic Files API wrapper.
23
+ """
24
+
25
+ # Files API is in beta and requires this header, per official docs
26
+ # https://docs.anthropic.com/en/docs/build-with-claude/files
27
+ BETA_HEADER = ["files-api-2025-04-14"]
28
+
29
+ def __init__(self, window=None):
30
+ """
31
+ Anthropic Files API store wrapper
32
+
33
+ :param window: Window instance
34
+ """
35
+ self.window = window
36
+ self.importer = Importer(window)
37
+
38
+ def get_client(self):
39
+ """
40
+ Get Anthropic client
41
+
42
+ :return: anthropic.Anthropic
43
+ """
44
+ return self.window.core.api.anthropic.get_client()
45
+
46
+ def log(self, msg: str, callback: Optional[callable] = None):
47
+ """
48
+ Log message
49
+
50
+ :param msg: message to log
51
+ :param callback: callback log function
52
+ """
53
+ if callback is not None:
54
+ callback(msg)
55
+ else:
56
+ print(msg)
57
+
58
+ def _download_dir(self) -> str:
59
+ """
60
+ Resolve target download directory (uses download.dir if set).
61
+ """
62
+ if self.window.core.config.has("download.dir") and self.window.core.config.get("download.dir") != "":
63
+ dir_path = os.path.join(
64
+ self.window.core.config.get_user_dir('data'),
65
+ self.window.core.config.get("download.dir"),
66
+ )
67
+ else:
68
+ dir_path = self.window.core.config.get_user_dir('data')
69
+ os.makedirs(dir_path, exist_ok=True)
70
+ return dir_path
71
+
72
+ def _ensure_unique_path(self, dir_path: str, filename: str) -> str:
73
+ """
74
+ Ensure unique filename in dir, add timestamp prefix if exists.
75
+ """
76
+ path = os.path.join(dir_path, filename)
77
+ if os.path.exists(path):
78
+ prefix = time.strftime("%Y%m%d_%H%M%S_")
79
+ path = os.path.join(dir_path, f"{prefix}{filename}")
80
+ return path
81
+
82
+ # -----------------------------
83
+ # Files service
84
+ # -----------------------------
85
+
86
+ def get_file(self, file_id: str):
87
+ """
88
+ Retrieve file metadata by ID using the SDK method names present in the installed version.
89
+ Prefer 'retrieve', fallback to 'get' only if available to avoid attribute errors.
90
+ """
91
+ client = self.get_client()
92
+ files_api = client.beta.files
93
+ if hasattr(files_api, "retrieve"):
94
+ return files_api.retrieve(file_id, betas=self.BETA_HEADER)
95
+ if hasattr(files_api, "get"):
96
+ return files_api.get(file_id, betas=self.BETA_HEADER)
97
+ raise AttributeError("Anthropic Files API client does not expose 'retrieve' or 'get' for file metadata.")
98
+
99
+ def upload(self, path: str):
100
+ """
101
+ Upload file to Anthropic Files API.
102
+
103
+ Per SDK guidance, the Python client supports passing a PathLike, raw bytes,
104
+ or a (filename, contents, media_type) tuple. Using the tuple form ensures the
105
+ correct filename and MIME type are sent in multipart/form-data.
106
+
107
+ :param path: file path
108
+ :return: file object or None
109
+ """
110
+ client = self.get_client()
111
+ if not os.path.exists(path):
112
+ return None
113
+
114
+ filename = os.path.basename(path)
115
+ mime_type = mimetypes.guess_type(path)[0] or "application/octet-stream"
116
+ with open(path, "rb") as f:
117
+ data = f.read()
118
+ file_part = (filename, data, mime_type)
119
+
120
+ files_api = client.beta.files
121
+
122
+ try:
123
+ return files_api.upload(file=file_part, betas=self.BETA_HEADER)
124
+ except Exception:
125
+ if hasattr(files_api, "create"):
126
+ return files_api.create(file=file_part, betas=self.BETA_HEADER)
127
+ raise
128
+
129
+ def delete_file(self, file_id: str) -> Optional[str]:
130
+ """
131
+ Delete a file by ID. Returns the file_id on success.
132
+ """
133
+ client = self.get_client()
134
+ res = client.beta.files.delete(file_id, betas=self.BETA_HEADER)
135
+ if res is not None:
136
+ return file_id
137
+
138
+ def download(self, file_id: str, path: str) -> bool:
139
+ """
140
+ Download a file content to a local path.
141
+
142
+ :param file_id: Anthropic Files API file id
143
+ :param path: target file path
144
+ :return: True on success
145
+ """
146
+ client = self.get_client()
147
+ try:
148
+ # SDK returns raw bytes for file content
149
+ data = client.beta.files.content(file_id, betas=self.BETA_HEADER)
150
+ except Exception:
151
+ data = None
152
+
153
+ if data is None:
154
+ return False
155
+
156
+ try:
157
+ if hasattr(data, "read"):
158
+ content = data.read()
159
+ else:
160
+ content = data if isinstance(data, (bytes, bytearray)) else bytes(data)
161
+ with open(path, "wb") as f:
162
+ f.write(content)
163
+ return True
164
+ except Exception:
165
+ return False
166
+
167
+ def download_to_dir(self, file_id: str, prefer_name: Optional[str] = None) -> Optional[str]:
168
+ """
169
+ Download a file by ID into configured download directory.
170
+
171
+ :param file_id: Anthropic Files API file id
172
+ :param prefer_name: optional filename preference
173
+ :return: saved file path or None
174
+ """
175
+ dir_path = self._download_dir()
176
+ filename = None
177
+
178
+ if prefer_name:
179
+ filename = os.path.basename(prefer_name)
180
+
181
+ if not filename:
182
+ try:
183
+ meta = self.get_file(file_id)
184
+ except Exception:
185
+ meta = None
186
+ if meta is not None:
187
+ for attr in ("filename", "name", "id"):
188
+ try:
189
+ val = getattr(meta, attr, None)
190
+ if isinstance(val, str) and val:
191
+ filename = os.path.basename(val)
192
+ break
193
+ except Exception:
194
+ pass
195
+ if not filename:
196
+ filename = file_id
197
+ else:
198
+ filename = file_id
199
+
200
+ if not os.path.splitext(filename)[1] and meta is not None:
201
+ try:
202
+ mime = getattr(meta, "mime_type", None)
203
+ ext = mimetypes.guess_extension(mime or "") or ""
204
+ if ext and not filename.endswith(ext):
205
+ filename = filename + ext
206
+ except Exception:
207
+ pass
208
+
209
+ path = self._ensure_unique_path(dir_path, filename)
210
+ if self.download(file_id, path):
211
+ return path
212
+ return None
213
+
214
+ def get_files_ids(self, limit: int = 1000) -> List[str]:
215
+ """
216
+ Return a list of file IDs. Falls back to filename only if id is missing.
217
+ """
218
+ client = self.get_client()
219
+ items: List[str] = []
220
+ pager = client.beta.files.list(limit=limit, betas=self.BETA_HEADER)
221
+
222
+ data = getattr(pager, "data", None)
223
+ if data is None:
224
+ data = pager
225
+
226
+ for f in data:
227
+ fid = getattr(f, "id", None) or getattr(f, "filename", None)
228
+ if fid and fid not in items:
229
+ items.append(fid)
230
+ return items
231
+
232
+ def remove_files(self, callback: Optional[callable] = None) -> int:
233
+ """
234
+ Remove all files from remote storage. Returns number of successfully removed files.
235
+ """
236
+ num = 0
237
+ files = self.get_files_ids()
238
+ for file_id in files:
239
+ self.log("Removing file: " + file_id, callback)
240
+ try:
241
+ res = self.delete_file(file_id)
242
+ if res:
243
+ num += 1
244
+ except Exception as e:
245
+ msg = "Error removing file {}: {}".format(file_id, str(e))
246
+ self.log(msg, callback)
247
+ return num
248
+
249
+ def remove_file(self, file_id: str, callback: Optional[callable] = None) -> bool:
250
+ """
251
+ Remove a single file by ID. Raises on errors to allow upstream handling.
252
+ """
253
+ self.log("Removing file: " + file_id, callback)
254
+ try:
255
+ res = self.delete_file(file_id)
256
+ return res is not None
257
+ except Exception as e:
258
+ msg = "Error removing file {}: {}".format(file_id, str(e))
259
+ self.log(msg, callback)
260
+ raise
261
+
262
+ def import_files(self, callback: Optional[callable] = None) -> int:
263
+ """
264
+ Import all files from Anthropic Files API into local DB.
265
+
266
+ :param callback: log callback
267
+ :return: number of imported files
268
+ """
269
+ client = self.get_client()
270
+ total = 0
271
+ pager = client.beta.files.list(limit=1000, betas=self.BETA_HEADER)
272
+
273
+ data = getattr(pager, "data", None)
274
+ if data is None:
275
+ data = pager
276
+
277
+ for f in data:
278
+ try:
279
+ self.window.core.remote_store.anthropic.files.insert("files", f)
280
+ total += 1
281
+ except Exception as e:
282
+ self.log("Error importing file {}: {}".format(getattr(f, "id", "?"), e), callback)
283
+ return total
284
+
285
+ def get_files_stats(self) -> Dict[str, Any]:
286
+ """
287
+ Compute files stats (count + total bytes).
288
+ """
289
+ client = self.get_client()
290
+ count = 0
291
+ total_bytes = 0
292
+ pager = client.beta.files.list(limit=1000, betas=self.BETA_HEADER)
293
+
294
+ data = getattr(pager, "data", None)
295
+ if data is None:
296
+ data = pager
297
+
298
+ for f in data:
299
+ count += 1
300
+ try:
301
+ size = getattr(f, "size_bytes", None)
302
+ if size is None and hasattr(f, "size"):
303
+ size = getattr(f, "size")
304
+ total_bytes += int(size or 0)
305
+ except Exception:
306
+ pass
307
+ return {"count": count, "total_bytes": total_bytes}
@@ -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,82 @@ 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
+
43
+ def _to_plain(obj):
44
+ try:
45
+ if hasattr(obj, "model_dump"):
46
+ return obj.model_dump()
47
+ if hasattr(obj, "to_dict"):
48
+ return obj.to_dict()
49
+ except Exception:
50
+ pass
51
+ if isinstance(obj, dict):
52
+ return {k: _to_plain(v) for k, v in obj.items()}
53
+ if isinstance(obj, (list, tuple)):
54
+ return [_to_plain(x) for x in obj]
55
+ return obj
56
+
57
+ def _walk_for_file_ids(o, acc: set):
58
+ if o is None:
59
+ return
60
+ if isinstance(o, dict):
61
+ for k, v in o.items():
62
+ if k == "file_id" and isinstance(v, str) and v.startswith("file_"):
63
+ acc.add(v)
64
+ else:
65
+ _walk_for_file_ids(v, acc)
66
+ elif isinstance(o, (list, tuple)):
67
+ for it in o:
68
+ _walk_for_file_ids(it, acc)
69
+
70
+ def _download_files(ids: set):
71
+ if not ids:
72
+ return
73
+ if not hasattr(state, "anthropic_downloaded_ids"):
74
+ state.anthropic_downloaded_ids = set()
75
+ saved = []
76
+ for fid in ids:
77
+ if fid in state.anthropic_downloaded_ids:
78
+ continue
79
+ try:
80
+ path = core.api.anthropic.store.download_to_dir(fid)
81
+ except Exception:
82
+ path = None
83
+ if path:
84
+ saved.append(path)
85
+ state.anthropic_downloaded_ids.add(fid)
86
+ if saved:
87
+ try:
88
+ loc = core.filesystem.make_local_list(saved)
89
+ except Exception:
90
+ loc = saved
91
+ if not isinstance(ctx.files, list):
92
+ ctx.files = []
93
+ for p in loc:
94
+ if p not in ctx.files:
95
+ ctx.files.append(p)
96
+ imgs = []
97
+ for p in loc:
98
+ ext = p.lower().rsplit(".", 1)[-1] if "." in p else ""
99
+ if ext in ["png", "jpg", "jpeg", "gif", "bmp", "tiff", "webp"]:
100
+ imgs.append(p)
101
+ if imgs:
102
+ if not isinstance(ctx.images, list):
103
+ ctx.images = []
104
+ for p in imgs:
105
+ if p not in ctx.images:
106
+ ctx.images.append(p)
31
107
 
32
108
  # --- Top-level delta objects (when SDK yields deltas directly) ---
33
109
  if etype == "text_delta":
@@ -37,7 +113,7 @@ def process_anthropic_chunk(ctx, core, state, chunk) -> Optional[str]:
37
113
  if etype == "thinking_delta":
38
114
  return None
39
115
 
40
- if etype == "input_json_delta":
116
+ if etype == "input_json_delta" and not is_computer_call:
41
117
  pj = getattr(chunk, "partial_json", "") or ""
42
118
  buf = state.fn_args_buffers.get("__anthropic_last__")
43
119
  if buf is None:
@@ -64,23 +140,36 @@ def process_anthropic_chunk(ctx, core, state, chunk) -> Optional[str]:
64
140
  pass
65
141
  return None
66
142
 
67
- if etype == "content_block_start":
143
+ if etype == "content_block_start" and not is_computer_call:
68
144
  try:
69
145
  cb = getattr(chunk, "content_block", None)
70
146
  if cb and getattr(cb, "type", "") == "tool_use":
71
147
  idx = getattr(chunk, "index", 0) or 0
72
148
  tid = getattr(cb, "id", "") or ""
73
149
  name = getattr(cb, "name", "") or ""
74
- state.tool_calls.append({
75
- "id": tid,
76
- "type": "function",
77
- "function": {"name": name, "arguments": ""}
78
- })
150
+ # Skip generic function-call for Anthropic Computer Use; the adapter will emit computer_call items.
151
+ if name not in {"computer", "computer.use", "anthropic/computer", "computer_use", "computer-use"}:
152
+ state.tool_calls.append({
153
+ "id": tid,
154
+ "type": "function",
155
+ "function": {"name": name, "arguments": ""}
156
+ })
79
157
  state.fn_args_buffers[str(idx)] = io.StringIO()
80
158
  state.fn_args_buffers["__anthropic_last__"] = state.fn_args_buffers[str(idx)]
81
159
  except Exception:
82
160
  pass
83
161
 
162
+ try:
163
+ cb = getattr(chunk, "content_block", None)
164
+ if cb:
165
+ btype = getattr(cb, "type", "") or ""
166
+ if btype.endswith("_tool_result"):
167
+ ids = set()
168
+ _walk_for_file_ids(_to_plain(getattr(cb, "content", None)), ids)
169
+ _download_files(ids)
170
+ except Exception:
171
+ pass
172
+
84
173
  try:
85
174
  cb = getattr(chunk, "content_block", None)
86
175
  if cb and getattr(cb, "type", "") == "web_search_tool_result":
@@ -97,7 +186,7 @@ def process_anthropic_chunk(ctx, core, state, chunk) -> Optional[str]:
97
186
 
98
187
  return None
99
188
 
100
- if etype == "content_block_delta":
189
+ if etype == "content_block_delta" and not is_computer_call:
101
190
  try:
102
191
  delta = getattr(chunk, "delta", None)
103
192
  if not delta:
@@ -125,7 +214,7 @@ def process_anthropic_chunk(ctx, core, state, chunk) -> Optional[str]:
125
214
  pass
126
215
  return response
127
216
 
128
- if etype == "content_block_stop":
217
+ if etype == "content_block_stop" and not is_computer_call:
129
218
  try:
130
219
  idx = str(getattr(chunk, "index", 0) or 0)
131
220
  buf = state.fn_args_buffers.pop(idx, None)