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.
Files changed (98) hide show
  1. pygpt_net/CHANGELOG.txt +12 -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/dialogs/confirm.py +35 -58
  9. pygpt_net/controller/lang/mapping.py +9 -9
  10. pygpt_net/controller/realtime/realtime.py +13 -1
  11. pygpt_net/controller/remote_store/{google/batch.py → batch.py} +209 -252
  12. pygpt_net/controller/remote_store/remote_store.py +982 -13
  13. pygpt_net/core/command/command.py +0 -0
  14. pygpt_net/core/db/viewer.py +1 -1
  15. pygpt_net/core/realtime/worker.py +3 -1
  16. pygpt_net/{controller/remote_store/google → core/remote_store/anthropic}/__init__.py +0 -1
  17. pygpt_net/core/remote_store/anthropic/files.py +211 -0
  18. pygpt_net/core/remote_store/anthropic/store.py +208 -0
  19. pygpt_net/core/remote_store/openai/store.py +5 -4
  20. pygpt_net/core/remote_store/remote_store.py +5 -1
  21. pygpt_net/{controller/remote_store/openai → core/remote_store/xai}/__init__.py +0 -1
  22. pygpt_net/core/remote_store/xai/files.py +225 -0
  23. pygpt_net/core/remote_store/xai/store.py +219 -0
  24. pygpt_net/data/config/config.json +10 -6
  25. pygpt_net/data/config/models.json +38 -22
  26. pygpt_net/data/config/settings.json +54 -1
  27. pygpt_net/data/icons/folder_eye.svg +1 -0
  28. pygpt_net/data/icons/folder_eye_filled.svg +1 -0
  29. pygpt_net/data/icons/folder_open.svg +1 -0
  30. pygpt_net/data/icons/folder_open_filled.svg +1 -0
  31. pygpt_net/data/locale/locale.de.ini +4 -3
  32. pygpt_net/data/locale/locale.en.ini +14 -4
  33. pygpt_net/data/locale/locale.es.ini +4 -3
  34. pygpt_net/data/locale/locale.fr.ini +4 -3
  35. pygpt_net/data/locale/locale.it.ini +4 -3
  36. pygpt_net/data/locale/locale.pl.ini +5 -4
  37. pygpt_net/data/locale/locale.uk.ini +4 -3
  38. pygpt_net/data/locale/locale.zh.ini +4 -3
  39. pygpt_net/icons.qrc +4 -0
  40. pygpt_net/icons_rc.py +282 -138
  41. pygpt_net/provider/api/anthropic/__init__.py +2 -0
  42. pygpt_net/provider/api/anthropic/chat.py +84 -1
  43. pygpt_net/provider/api/anthropic/store.py +307 -0
  44. pygpt_net/provider/api/anthropic/stream.py +75 -0
  45. pygpt_net/provider/api/anthropic/worker/__init__.py +0 -0
  46. pygpt_net/provider/api/anthropic/worker/importer.py +278 -0
  47. pygpt_net/provider/api/google/chat.py +59 -2
  48. pygpt_net/provider/api/google/realtime/client.py +70 -24
  49. pygpt_net/provider/api/google/realtime/realtime.py +48 -12
  50. pygpt_net/provider/api/google/store.py +124 -3
  51. pygpt_net/provider/api/google/stream.py +91 -24
  52. pygpt_net/provider/api/google/worker/importer.py +16 -28
  53. pygpt_net/provider/api/openai/assistants.py +2 -2
  54. pygpt_net/provider/api/openai/realtime/realtime.py +26 -6
  55. pygpt_net/provider/api/openai/store.py +4 -1
  56. pygpt_net/provider/api/openai/worker/importer.py +19 -61
  57. pygpt_net/provider/api/openai/worker/importer_assistants.py +230 -0
  58. pygpt_net/provider/api/x_ai/__init__.py +27 -6
  59. pygpt_net/provider/api/x_ai/audio.py +43 -11
  60. pygpt_net/provider/api/x_ai/chat.py +92 -4
  61. pygpt_net/provider/api/x_ai/realtime/__init__.py +12 -0
  62. pygpt_net/provider/api/x_ai/realtime/client.py +1864 -0
  63. pygpt_net/provider/api/x_ai/realtime/realtime.py +213 -0
  64. pygpt_net/provider/api/x_ai/remote_tools.py +102 -1
  65. pygpt_net/provider/api/x_ai/store.py +610 -0
  66. pygpt_net/provider/api/x_ai/stream.py +30 -9
  67. pygpt_net/provider/api/x_ai/tools.py +51 -0
  68. pygpt_net/provider/api/x_ai/worker/importer.py +308 -0
  69. pygpt_net/provider/audio_input/xai_grok_voice.py +390 -0
  70. pygpt_net/provider/audio_output/xai_tts.py +325 -0
  71. pygpt_net/provider/core/config/patch.py +29 -3
  72. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +2 -2
  73. pygpt_net/provider/core/model/patch.py +49 -1
  74. pygpt_net/tools/image_viewer/tool.py +334 -34
  75. pygpt_net/tools/image_viewer/ui/dialogs.py +317 -21
  76. pygpt_net/ui/dialog/assistant.py +1 -1
  77. pygpt_net/ui/dialog/plugins.py +13 -5
  78. pygpt_net/ui/dialog/remote_store.py +552 -0
  79. pygpt_net/ui/dialogs.py +3 -5
  80. pygpt_net/ui/layout/ctx/ctx_list.py +58 -7
  81. pygpt_net/ui/menu/tools.py +6 -13
  82. pygpt_net/ui/widget/dialog/{remote_store_google.py → remote_store.py} +10 -10
  83. pygpt_net/ui/widget/element/button.py +4 -4
  84. pygpt_net/ui/widget/image/display.py +2 -2
  85. pygpt_net/ui/widget/lists/context.py +2 -2
  86. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/METADATA +14 -2
  87. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/RECORD +87 -75
  88. pygpt_net/controller/remote_store/google/store.py +0 -615
  89. pygpt_net/controller/remote_store/openai/batch.py +0 -524
  90. pygpt_net/controller/remote_store/openai/store.py +0 -699
  91. pygpt_net/ui/dialog/remote_store_google.py +0 -539
  92. pygpt_net/ui/dialog/remote_store_openai.py +0 -539
  93. pygpt_net/ui/widget/dialog/remote_store_openai.py +0 -56
  94. pygpt_net/ui/widget/lists/remote_store_google.py +0 -248
  95. pygpt_net/ui/widget/lists/remote_store_openai.py +0 -317
  96. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/LICENSE +0 -0
  97. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.9.dist-info}/WHEEL +0 -0
  98. {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.04 19:00:00 #
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(