pygpt-net 2.7.7__py3-none-any.whl → 2.7.9__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 +12 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +5 -1
- pygpt_net/controller/assistant/batch.py +2 -2
- pygpt_net/controller/assistant/files.py +7 -6
- pygpt_net/controller/assistant/threads.py +0 -0
- pygpt_net/controller/chat/command.py +0 -0
- pygpt_net/controller/dialogs/confirm.py +35 -58
- pygpt_net/controller/lang/mapping.py +9 -9
- pygpt_net/controller/realtime/realtime.py +13 -1
- pygpt_net/controller/remote_store/{google/batch.py → batch.py} +209 -252
- pygpt_net/controller/remote_store/remote_store.py +982 -13
- pygpt_net/core/command/command.py +0 -0
- pygpt_net/core/db/viewer.py +1 -1
- pygpt_net/core/realtime/worker.py +3 -1
- pygpt_net/{controller/remote_store/google → core/remote_store/anthropic}/__init__.py +0 -1
- pygpt_net/core/remote_store/anthropic/files.py +211 -0
- pygpt_net/core/remote_store/anthropic/store.py +208 -0
- pygpt_net/core/remote_store/openai/store.py +5 -4
- pygpt_net/core/remote_store/remote_store.py +5 -1
- pygpt_net/{controller/remote_store/openai → core/remote_store/xai}/__init__.py +0 -1
- pygpt_net/core/remote_store/xai/files.py +225 -0
- pygpt_net/core/remote_store/xai/store.py +219 -0
- pygpt_net/data/config/config.json +10 -6
- pygpt_net/data/config/models.json +38 -22
- pygpt_net/data/config/settings.json +54 -1
- pygpt_net/data/icons/folder_eye.svg +1 -0
- pygpt_net/data/icons/folder_eye_filled.svg +1 -0
- pygpt_net/data/icons/folder_open.svg +1 -0
- pygpt_net/data/icons/folder_open_filled.svg +1 -0
- pygpt_net/data/locale/locale.de.ini +4 -3
- pygpt_net/data/locale/locale.en.ini +14 -4
- pygpt_net/data/locale/locale.es.ini +4 -3
- pygpt_net/data/locale/locale.fr.ini +4 -3
- pygpt_net/data/locale/locale.it.ini +4 -3
- pygpt_net/data/locale/locale.pl.ini +5 -4
- pygpt_net/data/locale/locale.uk.ini +4 -3
- pygpt_net/data/locale/locale.zh.ini +4 -3
- pygpt_net/icons.qrc +4 -0
- pygpt_net/icons_rc.py +282 -138
- pygpt_net/provider/api/anthropic/__init__.py +2 -0
- pygpt_net/provider/api/anthropic/chat.py +84 -1
- pygpt_net/provider/api/anthropic/store.py +307 -0
- pygpt_net/provider/api/anthropic/stream.py +75 -0
- pygpt_net/provider/api/anthropic/worker/__init__.py +0 -0
- pygpt_net/provider/api/anthropic/worker/importer.py +278 -0
- pygpt_net/provider/api/google/chat.py +59 -2
- pygpt_net/provider/api/google/realtime/client.py +70 -24
- pygpt_net/provider/api/google/realtime/realtime.py +48 -12
- pygpt_net/provider/api/google/store.py +124 -3
- pygpt_net/provider/api/google/stream.py +91 -24
- pygpt_net/provider/api/google/worker/importer.py +16 -28
- pygpt_net/provider/api/openai/assistants.py +2 -2
- pygpt_net/provider/api/openai/realtime/realtime.py +26 -6
- pygpt_net/provider/api/openai/store.py +4 -1
- pygpt_net/provider/api/openai/worker/importer.py +19 -61
- pygpt_net/provider/api/openai/worker/importer_assistants.py +230 -0
- pygpt_net/provider/api/x_ai/__init__.py +27 -6
- pygpt_net/provider/api/x_ai/audio.py +43 -11
- pygpt_net/provider/api/x_ai/chat.py +92 -4
- pygpt_net/provider/api/x_ai/realtime/__init__.py +12 -0
- pygpt_net/provider/api/x_ai/realtime/client.py +1864 -0
- pygpt_net/provider/api/x_ai/realtime/realtime.py +213 -0
- pygpt_net/provider/api/x_ai/remote_tools.py +102 -1
- pygpt_net/provider/api/x_ai/store.py +610 -0
- pygpt_net/provider/api/x_ai/stream.py +30 -9
- pygpt_net/provider/api/x_ai/tools.py +51 -0
- pygpt_net/provider/api/x_ai/worker/importer.py +308 -0
- pygpt_net/provider/audio_input/xai_grok_voice.py +390 -0
- pygpt_net/provider/audio_output/xai_tts.py +325 -0
- pygpt_net/provider/core/config/patch.py +29 -3
- pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +2 -2
- pygpt_net/provider/core/model/patch.py +49 -1
- pygpt_net/tools/image_viewer/tool.py +334 -34
- pygpt_net/tools/image_viewer/ui/dialogs.py +317 -21
- pygpt_net/ui/dialog/assistant.py +1 -1
- pygpt_net/ui/dialog/plugins.py +13 -5
- pygpt_net/ui/dialog/remote_store.py +552 -0
- pygpt_net/ui/dialogs.py +3 -5
- pygpt_net/ui/layout/ctx/ctx_list.py +58 -7
- pygpt_net/ui/menu/tools.py +6 -13
- pygpt_net/ui/widget/dialog/{remote_store_google.py → remote_store.py} +10 -10
- pygpt_net/ui/widget/element/button.py +4 -4
- pygpt_net/ui/widget/image/display.py +2 -2
- pygpt_net/ui/widget/lists/context.py +2 -2
- {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/METADATA +14 -2
- {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/RECORD +87 -75
- pygpt_net/controller/remote_store/google/store.py +0 -615
- pygpt_net/controller/remote_store/openai/batch.py +0 -524
- pygpt_net/controller/remote_store/openai/store.py +0 -699
- pygpt_net/ui/dialog/remote_store_google.py +0 -539
- pygpt_net/ui/dialog/remote_store_openai.py +0 -539
- pygpt_net/ui/widget/dialog/remote_store_openai.py +0 -56
- pygpt_net/ui/widget/lists/remote_store_google.py +0 -248
- pygpt_net/ui/widget/lists/remote_store_openai.py +0 -317
- {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/LICENSE +0 -0
- {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/WHEEL +0 -0
- {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,213 @@
|
|
|
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.07 23: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
|
+
xAI 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 = self.window.core.api.xai.tools.prepare_realtime(context.external_functions)
|
|
72
|
+
|
|
73
|
+
# remote tools
|
|
74
|
+
remote_tools = []
|
|
75
|
+
remote_tools = self.window.core.api.xai.remote.append_to_tools(
|
|
76
|
+
mode=context.mode,
|
|
77
|
+
model=model,
|
|
78
|
+
stream=context.stream,
|
|
79
|
+
is_expert_call=context.is_expert_call,
|
|
80
|
+
tools=remote_tools,
|
|
81
|
+
preset=context.preset,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# handle sub-reply (tool results from tool calls)
|
|
85
|
+
if context.ctx.internal:
|
|
86
|
+
if context.ctx.prev_ctx and context.ctx.prev_ctx.extra.get("prev_tool_calls"):
|
|
87
|
+
tool_calls = context.ctx.prev_ctx.extra.get("prev_tool_calls", [])
|
|
88
|
+
tool_call_id = None
|
|
89
|
+
if isinstance(tool_calls, list) and len(tool_calls) > 0:
|
|
90
|
+
tool_call_id = tool_calls[0].get("call_id", "") # get first call_id
|
|
91
|
+
if not tool_call_id:
|
|
92
|
+
tool_call_id = tool_calls[0].get("id", "") # fallback to id
|
|
93
|
+
if tool_call_id:
|
|
94
|
+
tool_results = context.ctx.input
|
|
95
|
+
try:
|
|
96
|
+
tool_results = json.loads(tool_results)
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
self.handler.send_tool_results_sync({
|
|
100
|
+
tool_call_id: tool_results
|
|
101
|
+
})
|
|
102
|
+
self.handler.update_ctx(context.ctx)
|
|
103
|
+
return True # do not start new session, just send tool results
|
|
104
|
+
|
|
105
|
+
# Resolve last session ID from history only (no fallback)
|
|
106
|
+
last_session_id = extract_last_session_id(context.history) if context.history else None
|
|
107
|
+
if is_debug:
|
|
108
|
+
print("[realtime session] Last ID", last_session_id)
|
|
109
|
+
|
|
110
|
+
# Enforce clean state rules prior to any live updates:
|
|
111
|
+
# - If there is no history: always reset live session to start fresh.
|
|
112
|
+
# - If history exists but has no resumable handle: close any active session to avoid continuation.
|
|
113
|
+
try:
|
|
114
|
+
history_len = len(context.history) if context.history else 0
|
|
115
|
+
except Exception:
|
|
116
|
+
history_len = 0
|
|
117
|
+
|
|
118
|
+
if history_len == 0:
|
|
119
|
+
if self.handler.is_session_active():
|
|
120
|
+
self.handler.close_session_sync()
|
|
121
|
+
try:
|
|
122
|
+
if context.ctx and isinstance(context.ctx.extra, dict):
|
|
123
|
+
context.ctx.extra.pop("rt_session_id", None)
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
last_session_id = None # force new session
|
|
127
|
+
elif not last_session_id and self.handler.is_session_active():
|
|
128
|
+
self.handler.close_session_sync()
|
|
129
|
+
|
|
130
|
+
# update auto-turn in active session
|
|
131
|
+
if (self.handler.is_session_active()
|
|
132
|
+
and (auto_turn != self.prev_auto_turn
|
|
133
|
+
or opt_vad_silence != self.prev_vad_silence
|
|
134
|
+
or opt_vad_prefix != self.prev_vad_prefix)):
|
|
135
|
+
self.handler.update_session_autoturn_sync(auto_turn, opt_vad_silence, opt_vad_prefix)
|
|
136
|
+
|
|
137
|
+
# if auto-turn is enabled and prompt is empty, update session and context only
|
|
138
|
+
if auto_turn and self.handler.is_session_active() and (context.prompt.strip() == "" or context.prompt == "..."):
|
|
139
|
+
self.handler.update_session_tools_sync(tools, remote_tools)
|
|
140
|
+
self.handler.update_ctx(context.ctx)
|
|
141
|
+
self.window.update_status(trans("speech.listening"))
|
|
142
|
+
return True # do not send new request if session is active
|
|
143
|
+
|
|
144
|
+
# Voice
|
|
145
|
+
voice = "Ara"
|
|
146
|
+
try:
|
|
147
|
+
v = self.window.core.plugins.get_option("audio_output", "xai_tts_voice")
|
|
148
|
+
if v:
|
|
149
|
+
voice = str(v)
|
|
150
|
+
except Exception:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
# Options
|
|
154
|
+
opts = RealtimeOptions(
|
|
155
|
+
provider=self.PROVIDER,
|
|
156
|
+
model=context.model.id,
|
|
157
|
+
system_prompt=context.system_prompt,
|
|
158
|
+
prompt=context.prompt,
|
|
159
|
+
voice=voice,
|
|
160
|
+
audio_data=audio_bytes,
|
|
161
|
+
audio_format=audio_format,
|
|
162
|
+
audio_rate=audio_rate,
|
|
163
|
+
vad="server_vad",
|
|
164
|
+
extra=extra or {},
|
|
165
|
+
tools=tools,
|
|
166
|
+
remote_tools=remote_tools,
|
|
167
|
+
rt_signals=rt_signals,
|
|
168
|
+
rt_session_id=last_session_id,
|
|
169
|
+
auto_turn=auto_turn,
|
|
170
|
+
vad_end_silence_ms=opt_vad_silence,
|
|
171
|
+
vad_prefix_padding_ms=opt_vad_prefix,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Start or append to realtime session via manager
|
|
175
|
+
try:
|
|
176
|
+
if is_debug:
|
|
177
|
+
print("[realtime] Starting session with options:", opts.to_dict())
|
|
178
|
+
rt = self.window.controller.realtime.manager
|
|
179
|
+
rt.start(context.ctx, opts)
|
|
180
|
+
|
|
181
|
+
self.prev_auto_turn = auto_turn
|
|
182
|
+
self.prev_vad_silence = opt_vad_silence
|
|
183
|
+
self.prev_vad_prefix = opt_vad_prefix
|
|
184
|
+
return True
|
|
185
|
+
except Exception as e:
|
|
186
|
+
self.window.core.debug.log(e)
|
|
187
|
+
return False # fallback to non-live path
|
|
188
|
+
|
|
189
|
+
def handle_audio_input(self, event: RealtimeEvent):
|
|
190
|
+
"""
|
|
191
|
+
Handle Realtime audio input event
|
|
192
|
+
|
|
193
|
+
:param event: RealtimeEvent
|
|
194
|
+
"""
|
|
195
|
+
self.handler.rt_handle_audio_input_sync(event)
|
|
196
|
+
|
|
197
|
+
def manual_commit(self):
|
|
198
|
+
"""Manually commit audio input to realtime session"""
|
|
199
|
+
self.handler.force_response_now_sync()
|
|
200
|
+
|
|
201
|
+
def shutdown(self):
|
|
202
|
+
"""Shutdown realtime loops"""
|
|
203
|
+
if self.handler.is_session_active():
|
|
204
|
+
self.handler.close_session_sync()
|
|
205
|
+
try:
|
|
206
|
+
self.handler.stop_loop_sync()
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
def reset(self):
|
|
211
|
+
"""Close realtime session"""
|
|
212
|
+
if self.handler.is_session_active():
|
|
213
|
+
self.handler.close_session_sync()
|
|
@@ -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.06 18:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
@@ -22,11 +22,13 @@ try:
|
|
|
22
22
|
from xai_sdk.tools import x_search as x_x_search
|
|
23
23
|
from xai_sdk.tools import code_execution as x_code_execution
|
|
24
24
|
from xai_sdk.tools import mcp as x_mcp
|
|
25
|
+
from xai_sdk.tools import collections_search as x_collections_search
|
|
25
26
|
except Exception:
|
|
26
27
|
x_web_search = None
|
|
27
28
|
x_x_search = None
|
|
28
29
|
x_code_execution = None
|
|
29
30
|
x_mcp = None
|
|
31
|
+
x_collections_search = None
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
class Remote:
|
|
@@ -41,6 +43,9 @@ class Remote:
|
|
|
41
43
|
- Builds xAI SDK tool objects for web_search, x_search, code_execution, MCP.
|
|
42
44
|
- Returns include flags, max_turns and use_encrypted_content settings.
|
|
43
45
|
|
|
46
|
+
Realtime (WebSocket) tools builder for xAI:
|
|
47
|
+
- Produces Realtime-compatible tool descriptors to be attached to session.update/response.create.
|
|
48
|
+
|
|
44
49
|
:param window: Window instance
|
|
45
50
|
"""
|
|
46
51
|
self.window = window
|
|
@@ -169,6 +174,23 @@ class Remote:
|
|
|
169
174
|
except Exception:
|
|
170
175
|
pass
|
|
171
176
|
|
|
177
|
+
# COLLECTIONS SEARCH
|
|
178
|
+
is_collections_enabled = bool(cfg.get("remote_tools.xai.collections", False))
|
|
179
|
+
ids = cfg.get("remote_tools.xai.collections.args", "")
|
|
180
|
+
ids_list = []
|
|
181
|
+
if ids:
|
|
182
|
+
try:
|
|
183
|
+
ids_list = [s.strip() for s in ids.split(",") if s.strip()]
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
if is_collections_enabled and ids_list:
|
|
187
|
+
try:
|
|
188
|
+
tools.append(x_collections_search(
|
|
189
|
+
collection_ids=ids_list
|
|
190
|
+
))
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
193
|
+
|
|
172
194
|
return {
|
|
173
195
|
"tools": tools,
|
|
174
196
|
"include": include,
|
|
@@ -251,6 +273,85 @@ class Remote:
|
|
|
251
273
|
"reason": http_reason,
|
|
252
274
|
}
|
|
253
275
|
|
|
276
|
+
# ---------- Realtime tools (WebSocket) ----------
|
|
277
|
+
|
|
278
|
+
def append_to_tools(
|
|
279
|
+
self,
|
|
280
|
+
mode: str,
|
|
281
|
+
model: ModelItem,
|
|
282
|
+
stream: bool,
|
|
283
|
+
is_expert_call: bool,
|
|
284
|
+
tools: List[dict],
|
|
285
|
+
preset=None
|
|
286
|
+
) -> List[dict]:
|
|
287
|
+
"""
|
|
288
|
+
Prepare remote tools for xAI Realtime sessions.
|
|
289
|
+
|
|
290
|
+
The returned list contains Realtime/WebSocket tool descriptors understood by xAI’s Grok Voice Agent API.
|
|
291
|
+
Server-side tools are executed by xAI; client-side tools are added separately in the caller.
|
|
292
|
+
|
|
293
|
+
:param mode: Agent mode (unused here, kept for signature compatibility)
|
|
294
|
+
:param model: Model item
|
|
295
|
+
:param stream: Streaming flag
|
|
296
|
+
:param is_expert_call: Expert call flag (unused here for xAI)
|
|
297
|
+
:param tools: Current tools list to extend
|
|
298
|
+
:param preset: Preset item (unused here)
|
|
299
|
+
:return: Extended tools list
|
|
300
|
+
"""
|
|
301
|
+
cfg = self.window.core.config
|
|
302
|
+
enabled_global = self.window.controller.chat.remote_tools.enabled
|
|
303
|
+
|
|
304
|
+
# Toggles
|
|
305
|
+
is_web_enabled = enabled_global(model, "web_search")
|
|
306
|
+
is_x_enabled = bool(cfg.get("remote_tools.xai.x_search", False))
|
|
307
|
+
is_code_enabled = bool(cfg.get("remote_tools.xai.code_execution", False))
|
|
308
|
+
is_mcp_enabled = bool(cfg.get("remote_tools.xai.mcp", False))
|
|
309
|
+
is_collections_enabled = bool(cfg.get("remote_tools.xai.collections", False))
|
|
310
|
+
|
|
311
|
+
# Web search
|
|
312
|
+
if is_web_enabled:
|
|
313
|
+
tools.append({"type": "web_search"})
|
|
314
|
+
|
|
315
|
+
# X search
|
|
316
|
+
if is_x_enabled:
|
|
317
|
+
tools.append({"type": "x_search"})
|
|
318
|
+
|
|
319
|
+
# Code execution (code interpreter)
|
|
320
|
+
if is_code_enabled:
|
|
321
|
+
# Container is optional here; xAI executes code server-side
|
|
322
|
+
tools.append({"type": "code_interpreter"})
|
|
323
|
+
|
|
324
|
+
# Collections search
|
|
325
|
+
if is_collections_enabled:
|
|
326
|
+
ids = cfg.get("remote_tools.xai.collections.args", "")
|
|
327
|
+
ids_list: List[str] = []
|
|
328
|
+
if ids:
|
|
329
|
+
try:
|
|
330
|
+
ids_list = [s.strip() for s in ids.split(",") if s.strip()]
|
|
331
|
+
except Exception:
|
|
332
|
+
ids_list = []
|
|
333
|
+
if ids_list:
|
|
334
|
+
tools.append({
|
|
335
|
+
"type": "collections_search",
|
|
336
|
+
"collection_ids": ids_list,
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
# MCP
|
|
340
|
+
if is_mcp_enabled:
|
|
341
|
+
mcp_tool = cfg.get("remote_tools.xai.mcp.args", "")
|
|
342
|
+
if mcp_tool:
|
|
343
|
+
try:
|
|
344
|
+
parsed = json.loads(mcp_tool)
|
|
345
|
+
# Accept either full dict or minimal {"type":"mcp"}
|
|
346
|
+
if isinstance(parsed, dict):
|
|
347
|
+
tools.append(parsed)
|
|
348
|
+
except Exception:
|
|
349
|
+
tools.append({"type": "mcp"})
|
|
350
|
+
else:
|
|
351
|
+
tools.append({"type": "mcp"})
|
|
352
|
+
|
|
353
|
+
return tools
|
|
354
|
+
|
|
254
355
|
# ---------- helpers ----------
|
|
255
356
|
|
|
256
357
|
def _build_http_params(
|