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,198 @@
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.06 20:00:00 #
10
+ # ================================================== #
11
+
12
+ import json
13
+ from typing import Optional, Dict, Any
14
+
15
+ from pygpt_net.core.bridge import BridgeContext
16
+ from pygpt_net.core.events import RealtimeEvent
17
+ from pygpt_net.core.realtime.options import RealtimeOptions
18
+ from pygpt_net.core.realtime.shared.session import extract_last_session_id
19
+ from pygpt_net.item.model import ModelItem
20
+ from pygpt_net.utils import trans
21
+
22
+ from .client import xAIIRealtimeClient
23
+
24
+ class Realtime:
25
+
26
+ PROVIDER = "x_ai"
27
+
28
+ def __init__(self, window=None):
29
+ """
30
+ OpenAI API realtime controller
31
+
32
+ :param window: Window instance
33
+ """
34
+ self.window = window
35
+ self.handler = xAIIRealtimeClient(window)
36
+ self.prev_auto_turn = False
37
+ self.prev_vad_silence = 2000
38
+ self.prev_vad_prefix = 300
39
+
40
+ def begin(
41
+ self,
42
+ context: BridgeContext,
43
+ model: Optional[ModelItem] = None,
44
+ extra: Optional[Dict[str, Any]] = None,
45
+ rt_signals=None
46
+ ) -> bool:
47
+ """
48
+ Begin realtime session if applicable
49
+
50
+ :param context: BridgeContext
51
+ :param model: Optional[ModelItem]
52
+ :param extra: Optional dict with extra parameters
53
+ :param rt_signals: RealtimeSignals
54
+ :return: True if realtime session started, False otherwise
55
+ """
56
+ mm = context.multimodal_ctx
57
+ audio_bytes = getattr(mm, "audio_data", None) if mm and getattr(mm, "is_audio_input", False) else None
58
+ audio_format = getattr(mm, "audio_format", None) if mm else None
59
+ audio_rate = getattr(mm, "audio_rate", None) if mm else None
60
+ is_debug = self.window.core.config.get("log.realtime", False)
61
+ auto_turn = self.window.core.config.get("audio.input.auto_turn", True)
62
+ opt_vad_silence = self.window.core.config.get("audio.input.vad.silence", 2000)
63
+ opt_vad_prefix = self.window.core.config.get("audio.input.vad.prefix", 300)
64
+
65
+ # setup manager
66
+ self.window.controller.realtime.set_current_active(self.PROVIDER)
67
+ self.window.controller.realtime.set_busy()
68
+ self.handler.set_debug(is_debug)
69
+
70
+ # tools
71
+ tools = []
72
+ '''
73
+ tools = self.window.core.api.xai.tools.prepare(model, context.external_functions)
74
+ '''
75
+
76
+ # remote tools
77
+ remote_tools = []
78
+ '''
79
+ remote_tools = self.window.core.api.openai.remote_tools.append_to_tools(
80
+ mode=context.mode,
81
+ model=model,
82
+ stream=context.stream,
83
+ is_expert_call=context.is_expert_call,
84
+ tools=remote_tools,
85
+ preset=context.preset,
86
+ )
87
+ '''
88
+
89
+ # handle sub-reply (tool results from tool calls)
90
+ if context.ctx.internal:
91
+ if context.ctx.prev_ctx and context.ctx.prev_ctx.extra.get("prev_tool_calls"):
92
+ tool_calls = context.ctx.prev_ctx.extra.get("prev_tool_calls", [])
93
+ tool_call_id = None
94
+ if isinstance(tool_calls, list) and len(tool_calls) > 0:
95
+ tool_call_id = tool_calls[0].get("call_id", "") # get first call_id
96
+ if not tool_call_id:
97
+ tool_call_id = tool_calls[0].get("id", "") # fallback to id
98
+ if tool_call_id:
99
+ tool_results = context.ctx.input
100
+ try:
101
+ tool_results = json.loads(tool_results)
102
+ except Exception:
103
+ pass
104
+ self.handler.send_tool_results_sync({
105
+ tool_call_id: tool_results
106
+ })
107
+ self.handler.update_ctx(context.ctx)
108
+ return True # do not start new session, just send tool results
109
+
110
+ # update auto-turn in active session
111
+ if (self.handler.is_session_active()
112
+ and (auto_turn != self.prev_auto_turn
113
+ or opt_vad_silence != self.prev_vad_silence
114
+ or opt_vad_prefix != self.prev_vad_prefix)):
115
+ self.handler.update_session_autoturn_sync(auto_turn, opt_vad_silence, opt_vad_prefix)
116
+
117
+ # if auto-turn is enabled and prompt is empty, update session and context only
118
+ if auto_turn and self.handler.is_session_active() and (context.prompt.strip() == "" or context.prompt == "..."):
119
+ self.handler.update_session_tools_sync(tools, remote_tools)
120
+ self.handler.update_ctx(context.ctx)
121
+ self.window.update_status(trans("speech.listening"))
122
+ return True # do not send new request if session is active
123
+
124
+ # Last session ID
125
+ last_session_id = extract_last_session_id(context.history)
126
+ if is_debug:
127
+ print("[realtime session] Last ID", last_session_id)
128
+
129
+ # Voice
130
+ voice = "ara"
131
+ try:
132
+ v = self.window.core.plugins.get_option("audio_output", "xai_tts_voice")
133
+ if v:
134
+ voice = str(v)
135
+ except Exception:
136
+ pass
137
+
138
+ # Options
139
+ opts = RealtimeOptions(
140
+ provider=self.PROVIDER,
141
+ model=context.model.id,
142
+ system_prompt=context.system_prompt,
143
+ prompt=context.prompt,
144
+ voice=voice,
145
+ audio_data=audio_bytes,
146
+ audio_format=audio_format,
147
+ audio_rate=audio_rate,
148
+ vad="server_vad",
149
+ extra=extra or {},
150
+ tools=tools,
151
+ remote_tools=remote_tools,
152
+ rt_signals=rt_signals,
153
+ rt_session_id=last_session_id,
154
+ auto_turn=auto_turn,
155
+ vad_end_silence_ms=opt_vad_silence,
156
+ vad_prefix_padding_ms=opt_vad_prefix,
157
+ )
158
+
159
+ # Start or append to realtime session via manager
160
+ try:
161
+ if is_debug:
162
+ print("[realtime] Starting session with options:", opts.to_dict())
163
+ rt = self.window.controller.realtime.manager
164
+ rt.start(context.ctx, opts)
165
+
166
+ self.prev_auto_turn = auto_turn
167
+ self.prev_vad_silence = opt_vad_silence
168
+ self.prev_vad_prefix = opt_vad_prefix
169
+ return True
170
+ except Exception as e:
171
+ self.window.core.debug.log(e)
172
+ return False # fallback to non-live path
173
+
174
+ def handle_audio_input(self, event: RealtimeEvent):
175
+ """
176
+ Handle Realtime audio input event
177
+
178
+ :param event: RealtimeEvent
179
+ """
180
+ self.handler.rt_handle_audio_input_sync(event)
181
+
182
+ def manual_commit(self):
183
+ """Manually commit audio input to realtime session"""
184
+ self.handler.force_response_now_sync()
185
+
186
+ def shutdown(self):
187
+ """Shutdown realtime loops"""
188
+ if self.handler.is_session_active():
189
+ self.handler.close_session_sync()
190
+ try:
191
+ self.handler.stop_loop_sync()
192
+ except Exception:
193
+ pass
194
+
195
+ def reset(self):
196
+ """Close realtime session"""
197
+ if self.handler.is_session_active():
198
+ self.handler.close_session_sync()
@@ -6,27 +6,194 @@
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 20:00:00 #
9
+ # Updated Date: 2026.01.06 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from __future__ import annotations
13
+
14
+ import json
13
15
  from typing import Optional, Dict, Any, List
14
16
 
15
17
  from pygpt_net.item.model import ModelItem
16
18
 
19
+ # xAI server-side tools
20
+ try:
21
+ from xai_sdk.tools import web_search as x_web_search
22
+ from xai_sdk.tools import x_search as x_x_search
23
+ from xai_sdk.tools import code_execution as x_code_execution
24
+ from xai_sdk.tools import mcp as x_mcp
25
+ from xai_sdk.tools import collections_search as x_collections_search
26
+ except Exception:
27
+ x_web_search = None
28
+ x_x_search = None
29
+ x_code_execution = None
30
+ x_mcp = None
31
+
17
32
 
18
33
  class Remote:
19
34
  def __init__(self, window=None):
20
35
  """
21
- Live Search builder for xAI:
36
+ Live Search builder for xAI (old, grok-3):
22
37
  - Returns a dict with 'sdk' (X-only toggle) and 'http' (full search_parameters).
23
38
  - SDK path: native streaming (xai_sdk.chat.stream()) works only for basic X search (no advanced filters).
24
39
  - HTTP path: full Live Search (web/news/rss/X with filters), streaming via SSE.
25
40
 
41
+ Server-side Tools builder for xAI (newer SDKs, Responses API):
42
+ - Builds xAI SDK tool objects for web_search, x_search, code_execution, MCP.
43
+ - Returns include flags, max_turns and use_encrypted_content settings.
44
+
26
45
  :param window: Window instance
27
46
  """
28
47
  self.window = window
29
48
 
49
+ def build_for_chat(self, model: ModelItem = None, stream: bool = False) -> Dict[str, Any]:
50
+ """
51
+ Build server-side tools and options for Chat Responses.
52
+
53
+ Returns:
54
+ {
55
+ "tools": [ ... xai_sdk.tools.* ... ],
56
+ "include": [ ... ],
57
+ "use_encrypted_content": bool,
58
+ "max_turns": Optional[int],
59
+ }
60
+ """
61
+ cfg = self.window.core.config
62
+ include: List[str] = []
63
+ tools: List[object] = []
64
+
65
+ # global remote tools switch
66
+ is_web_enabled = self.window.controller.chat.remote_tools.enabled(model, "web_search")
67
+ is_x_enabled = bool(cfg.get("remote_tools.xai.x_search", False))
68
+ is_code_enabled = bool(cfg.get("remote_tools.xai.code_execution", False))
69
+ is_mcp_enabled = bool(cfg.get("remote_tools.xai.mcp", False))
70
+
71
+ # include flags
72
+ if stream:
73
+ include.append("verbose_streaming")
74
+ if bool(cfg.get("remote_tools.xai.inline_citations", True)):
75
+ include.append("inline_citations")
76
+ if bool(cfg.get("remote_tools.xai.include_code_output", True)):
77
+ include.append("code_execution_call_output")
78
+
79
+ # use_encrypted_content
80
+ use_encrypted = bool(cfg.get("remote_tools.xai.use_encrypted_content", False))
81
+
82
+ # optional max_turns
83
+ max_turns = None
84
+ try:
85
+ mt = cfg.get("remote_tools.xai.max_turns")
86
+ if isinstance(mt, int) and mt > 0:
87
+ max_turns = int(mt)
88
+ except Exception:
89
+ pass
90
+
91
+ # WEB SEARCH
92
+ if is_web_enabled and x_web_search is not None:
93
+ kwargs: Dict[str, Any] = {}
94
+ enable_img = bool(cfg.get("remote_tools.xai.web.enable_image_understanding", False))
95
+ '''
96
+ allowed = self._as_list(cfg.get("remote_tools.xai.web.allowed_websites"), 5)
97
+ excluded = self._as_list(cfg.get("remote_tools.xai.web.excluded_websites"), 5)
98
+ if allowed and not excluded:
99
+ kwargs["allowed_domains"] = allowed
100
+ elif excluded and not allowed:
101
+ kwargs["excluded_domains"] = excluded
102
+ '''
103
+ if enable_img:
104
+ kwargs["enable_image_understanding"] = True
105
+ try:
106
+ tools.append(x_web_search(**kwargs))
107
+ except Exception:
108
+ tools.append(x_web_search())
109
+
110
+ # X SEARCH
111
+ if is_x_enabled and x_x_search is not None:
112
+ kwargs: Dict[str, Any] = {}
113
+ '''
114
+ inc = self._as_list(cfg.get("remote_tools.xai.x.included_handles"), 10)
115
+ exc = self._as_list(cfg.get("remote_tools.xai.x.excluded_handles"), 10)
116
+ if inc and not exc:
117
+ kwargs["allowed_x_handles"] = inc
118
+ elif exc and not inc:
119
+ kwargs["excluded_x_handles"] = exc
120
+ '''
121
+ # optional date range filters (YYYY-MM-DD)
122
+ for k_in, k_out in (("remote_tools.xai.from_date", "from_date"),
123
+ ("remote_tools.xai.to_date", "to_date")):
124
+ v = cfg.get(k_in)
125
+ if isinstance(v, str) and v.strip():
126
+ kwargs[k_out] = v.strip()
127
+
128
+ if bool(cfg.get("remote_tools.xai.x.enable_image_understanding", False)):
129
+ kwargs["enable_image_understanding"] = True
130
+ if bool(cfg.get("remote_tools.xai.x.enable_video_understanding", False)):
131
+ kwargs["enable_video_understanding"] = True
132
+
133
+ # optional favorites/views filters (supported by live search)
134
+ try:
135
+ favs = cfg.get("remote_tools.xai.x.min_favs")
136
+ if isinstance(favs, int) and favs > 0:
137
+ kwargs["post_favorite_count"] = int(favs)
138
+ except Exception:
139
+ pass
140
+ try:
141
+ views = cfg.get("remote_tools.xai.x.min_views")
142
+ if isinstance(views, int) and views > 0:
143
+ kwargs["post_view_count"] = int(views)
144
+ except Exception:
145
+ pass
146
+
147
+ try:
148
+ tools.append(x_x_search(**kwargs))
149
+ except Exception:
150
+ tools.append(x_x_search())
151
+
152
+ # CODE EXECUTION
153
+ if is_code_enabled and x_code_execution is not None:
154
+ try:
155
+ tools.append(x_code_execution())
156
+ except Exception:
157
+ pass
158
+
159
+ # MCP
160
+ if is_mcp_enabled and x_mcp is not None:
161
+ kwargs = {}
162
+ mcp_config = cfg.get("remote_tools.xai.mcp.args", "")
163
+ if mcp_config:
164
+ try:
165
+ kwargs = json.loads(mcp_config)
166
+ except Exception:
167
+ pass
168
+ try:
169
+ tools.append(x_mcp(**kwargs))
170
+ except Exception:
171
+ pass
172
+
173
+ # COLLECTIONS SEARCH
174
+ is_collections_enabled = bool(cfg.get("remote_tools.xai.collections", False))
175
+ ids = cfg.get("remote_tools.xai.collections.args", "")
176
+ ids_list = []
177
+ if ids:
178
+ try:
179
+ ids_list = [s.strip() for s in ids.split(",") if s.strip()]
180
+ except Exception:
181
+ pass
182
+ if is_collections_enabled and ids_list:
183
+ try:
184
+ tools.append(x_collections_search(
185
+ collection_ids=ids_list
186
+ ))
187
+ except Exception:
188
+ pass
189
+
190
+ return {
191
+ "tools": tools,
192
+ "include": include,
193
+ "use_encrypted_content": use_encrypted,
194
+ "max_turns": max_turns,
195
+ }
196
+
30
197
  def build_remote_tools(self, model: ModelItem = None) -> Dict[str, Any]:
31
198
  """
32
199
  Return live-search config for xAI:
@@ -58,38 +225,17 @@ class Remote:
58
225
  """
59
226
  cfg = self.window.core.config
60
227
  is_web = self.window.controller.chat.remote_tools.enabled(model, "web_search") # get global config
61
-
62
- mode = str(cfg.get("remote_tools.xai.mode") or "auto").lower()
63
- if mode not in ("auto", "on", "off"):
64
- mode = "auto"
65
-
66
- if mode == "off":
67
- if is_web:
68
- mode = "on" # override off if global web_search enabled
228
+ mode = "on" if is_web else "off"
69
229
 
70
230
  # sources toggles
71
- s_web = bool(cfg.get("remote_tools.xai.sources.web", True))
72
- s_x = bool(cfg.get("remote_tools.xai.sources.x", True))
73
- s_news = bool(cfg.get("remote_tools.xai.sources.news", False))
74
- s_rss = bool(cfg.get("remote_tools.xai.sources.rss", False))
75
-
76
- # advanced flags
77
- adv_web_allowed = self._has_list(cfg.get("remote_tools.xai.web.allowed_websites"))
78
- adv_web_excluded = self._has_list(cfg.get("remote_tools.xai.web.excluded_websites"))
79
- adv_web_country = self._has_str(cfg.get("remote_tools.xai.web.country"))
80
- adv_web_safe = cfg.get("remote_tools.xai.web.safe_search", None)
81
-
82
- adv_news_excl = self._has_list(cfg.get("remote_tools.xai.news.excluded_websites"))
83
- adv_news_country = self._has_str(cfg.get("remote_tools.xai.news.country"))
84
- adv_news_safe = cfg.get("remote_tools.xai.news.safe_search", None)
231
+ s_web = bool(cfg.get("remote_tools.xai.web_search", False))
232
+ s_x = bool(cfg.get("remote_tools.xai.x_search", False))
85
233
 
86
234
  adv_x_incl = self._has_list(cfg.get("remote_tools.xai.x.included_handles"))
87
235
  adv_x_excl = self._has_list(cfg.get("remote_tools.xai.x.excluded_handles"))
88
236
  adv_x_favs = self._has_int(cfg.get("remote_tools.xai.x.min_favs"))
89
237
  adv_x_views = self._has_int(cfg.get("remote_tools.xai.x.min_views"))
90
238
 
91
- adv_rss_link = self._has_str(cfg.get("remote_tools.xai.rss.link"))
92
-
93
239
  adv_from = self._has_str(cfg.get("remote_tools.xai.from_date"))
94
240
  adv_to = self._has_str(cfg.get("remote_tools.xai.to_date"))
95
241
 
@@ -97,7 +243,7 @@ class Remote:
97
243
  adv_return_cits = cfg.get("remote_tools.xai.return_citations", True) is not True # different than default?
98
244
 
99
245
  # SDK-capable if: mode!=off and ONLY X is enabled and no X filters/date/max_results customizations
100
- x_only = s_x and not s_web and not s_news and not s_rss
246
+ x_only = s_x and not s_web
101
247
  x_filters = any([adv_x_incl, adv_x_excl, adv_x_favs, adv_x_views])
102
248
  sdk_enabled = (mode != "off") and x_only and not any([
103
249
  x_filters, adv_from, adv_to, adv_max_results, adv_return_cits
@@ -110,10 +256,10 @@ class Remote:
110
256
 
111
257
  need_http = (mode != "off") and (
112
258
  not sdk_enabled or # advanced X filters or other sources/date/results/citations
113
- s_web or s_news or s_rss
259
+ s_web
114
260
  )
115
261
  if need_http:
116
- http_params = self._build_http_params(cfg, mode, s_web, s_x, s_news, s_rss)
262
+ http_params = self._build_http_params(cfg, mode, s_web, s_x)
117
263
  http_reason = "advanced_sources_or_filters"
118
264
 
119
265
  return {
@@ -131,8 +277,6 @@ class Remote:
131
277
  mode: str,
132
278
  s_web: bool,
133
279
  s_x: bool,
134
- s_news: bool,
135
- s_rss: bool
136
280
  ) -> dict:
137
281
  """
138
282
  Build search_parameters for Chat Completions (HTTP).
@@ -141,8 +285,6 @@ class Remote:
141
285
  :param mode: "auto"|"on"|"off"
142
286
  :param s_web: Include web search
143
287
  :param s_x: Include X search
144
- :param s_news: Include news search
145
- :param s_rss: Include RSS search
146
288
  :return: search_parameters dict
147
289
  """
148
290
  params: Dict[str, Any] = {"mode": mode}
@@ -196,41 +338,12 @@ class Remote:
196
338
  xsrc["post_view_count"] = int(views)
197
339
  sources.append(xsrc)
198
340
 
199
- if s_news:
200
- news: Dict[str, Any] = {"type": "news"}
201
- country = cfg.get("remote_tools.xai.news.country")
202
- if isinstance(country, str) and len(country.strip()) == 2:
203
- news["country"] = country.strip().upper()
204
- excluded = self._as_list(cfg.get("remote_tools.xai.news.excluded_websites"), 5)
205
- if excluded:
206
- news["excluded_websites"] = excluded
207
- safe = cfg.get("remote_tools.xai.news.safe_search")
208
- if safe is not None:
209
- news["safe_search"] = bool(safe)
210
- sources.append(news)
211
-
212
- if s_rss:
213
- link = cfg.get("remote_tools.xai.rss.link")
214
- if isinstance(link, str) and link.strip():
215
- sources.append({"type": "rss", "links": [link.strip()]})
216
-
217
341
  if sources:
218
342
  params["sources"] = sources
219
343
 
220
344
  return params
221
345
 
222
- def _has_list(self, v) -> bool:
223
- """
224
- Return True if v is a non-empty list/tuple or a non-empty comma-separated string.
225
-
226
- :param v: Any
227
- :return: bool
228
- """
229
- if v is None:
230
- return False
231
- if isinstance(v, (list, tuple)):
232
- return len([x for x in v if str(x).strip()]) > 0
233
- return len([x for x in str(v).split(",") if x.strip()]) > 0
346
+ # ---------- helpers ----------
234
347
 
235
348
  def _has_str(self, v) -> bool:
236
349
  """
@@ -250,14 +363,14 @@ class Remote:
250
363
  """
251
364
  return isinstance(v, int) and v > 0
252
365
 
253
- def _as_list(self, v, limit: int) -> List[str]:
254
- """
255
- Convert v to a list of strings, limited to 'limit' items.
366
+ def _has_list(self, v) -> bool:
367
+ if v is None:
368
+ return False
369
+ if isinstance(v, (list, tuple)):
370
+ return len([x for x in v if str(x).strip()]) > 0
371
+ return len([x for x in str(v).split(",") if x.strip()]) > 0
256
372
 
257
- :param v: Any
258
- :param limit: limit number of items
259
- :return: List of strings
260
- """
373
+ def _as_list(self, v, limit: int) -> List[str]:
261
374
  if v is None:
262
375
  return []
263
376
  if isinstance(v, (list, tuple)):