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
|
@@ -362,6 +362,12 @@ class Chat:
|
|
|
362
362
|
except Exception:
|
|
363
363
|
pass
|
|
364
364
|
|
|
365
|
+
# Download Files API file_data parts if present
|
|
366
|
+
try:
|
|
367
|
+
self._maybe_download_response_files(response, ctx)
|
|
368
|
+
except Exception:
|
|
369
|
+
pass
|
|
370
|
+
|
|
365
371
|
def extract_text(self, response) -> str:
|
|
366
372
|
"""
|
|
367
373
|
Extract output text.
|
|
@@ -792,7 +798,7 @@ class Chat:
|
|
|
792
798
|
return bytes(data)
|
|
793
799
|
if isinstance(data, str):
|
|
794
800
|
import base64
|
|
795
|
-
return base64.b64decode(data)
|
|
801
|
+
return base64.b64encode(bytes()) if data == "" else base64.b64decode(data)
|
|
796
802
|
except Exception:
|
|
797
803
|
return None
|
|
798
804
|
return None
|
|
@@ -1000,4 +1006,55 @@ class Chat:
|
|
|
1000
1006
|
out.append({"type": typ, "uri": uri})
|
|
1001
1007
|
continue
|
|
1002
1008
|
|
|
1003
|
-
return out
|
|
1009
|
+
return out
|
|
1010
|
+
|
|
1011
|
+
def _maybe_download_response_files(self, response, ctx: CtxItem) -> None:
|
|
1012
|
+
"""
|
|
1013
|
+
Inspect non-stream response parts for Files API references and download them.
|
|
1014
|
+
"""
|
|
1015
|
+
try:
|
|
1016
|
+
cands = getattr(response, "candidates", None) or []
|
|
1017
|
+
if not cands:
|
|
1018
|
+
return
|
|
1019
|
+
first = cands[0]
|
|
1020
|
+
content = getattr(first, "content", None)
|
|
1021
|
+
parts = getattr(content, "parts", None) or []
|
|
1022
|
+
except Exception:
|
|
1023
|
+
parts = []
|
|
1024
|
+
|
|
1025
|
+
if not parts:
|
|
1026
|
+
return
|
|
1027
|
+
|
|
1028
|
+
downloaded: List[str] = []
|
|
1029
|
+
for p in parts:
|
|
1030
|
+
fdata = getattr(p, "file_data", None)
|
|
1031
|
+
if not fdata:
|
|
1032
|
+
continue
|
|
1033
|
+
try:
|
|
1034
|
+
uri = getattr(fdata, "file_uri", None) or getattr(fdata, "uri", None)
|
|
1035
|
+
prefer = getattr(fdata, "file_name", None) or getattr(fdata, "display_name", None)
|
|
1036
|
+
if not uri or not isinstance(uri, str):
|
|
1037
|
+
continue
|
|
1038
|
+
# Only Gemini Files API refs are supported for direct download
|
|
1039
|
+
save_path = self.window.core.api.google.store.download_to_dir(uri, prefer_name=prefer)
|
|
1040
|
+
if save_path:
|
|
1041
|
+
downloaded.append(save_path)
|
|
1042
|
+
except Exception:
|
|
1043
|
+
continue
|
|
1044
|
+
|
|
1045
|
+
if downloaded:
|
|
1046
|
+
downloaded = self.window.core.filesystem.make_local_list(downloaded)
|
|
1047
|
+
if not isinstance(ctx.files, list):
|
|
1048
|
+
ctx.files = []
|
|
1049
|
+
for path in downloaded:
|
|
1050
|
+
if path not in ctx.files:
|
|
1051
|
+
ctx.files.append(path)
|
|
1052
|
+
images = []
|
|
1053
|
+
for path in downloaded:
|
|
1054
|
+
ext = os.path.splitext(path)[1].lower().lstrip(".")
|
|
1055
|
+
if ext in ["png", "jpg", "jpeg", "gif", "bmp", "tiff", "webp"]:
|
|
1056
|
+
images.append(path)
|
|
1057
|
+
if images:
|
|
1058
|
+
if not isinstance(ctx.images, list):
|
|
1059
|
+
ctx.images = []
|
|
1060
|
+
ctx.images += images
|
|
@@ -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:
|
|
9
|
+
# Updated Date: 2026.01.07 23:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import asyncio
|
|
@@ -338,20 +338,25 @@ class GoogleLiveClient:
|
|
|
338
338
|
if sys_prompt:
|
|
339
339
|
live_cfg["system_instruction"] = str(sys_prompt)
|
|
340
340
|
|
|
341
|
-
#
|
|
341
|
+
# Save callbacks and ctx early so handle persistence can target the current context
|
|
342
|
+
self._on_text = on_text
|
|
343
|
+
self._on_audio = on_audio
|
|
344
|
+
self._should_stop = should_stop or (lambda: False)
|
|
345
|
+
self._ctx = ctx
|
|
346
|
+
self._last_opts = opts
|
|
347
|
+
|
|
348
|
+
# Session resumption: configure per docs; include handle when provided, otherwise None.
|
|
342
349
|
try:
|
|
350
|
+
ph = None
|
|
343
351
|
provided_handle = getattr(opts, "rt_session_id", None)
|
|
344
|
-
resume_handle = None
|
|
345
352
|
if isinstance(provided_handle, str):
|
|
346
|
-
ph = provided_handle.strip()
|
|
347
|
-
if ph and ph != (self._rt_session_id or ""):
|
|
348
|
-
resume_handle = ph
|
|
353
|
+
ph = provided_handle.strip() or None
|
|
349
354
|
|
|
350
|
-
|
|
355
|
+
sr_cfg = gtypes.SessionResumptionConfig(handle=ph)
|
|
356
|
+
live_cfg["session_resumption"] = sr_cfg
|
|
351
357
|
|
|
352
|
-
if
|
|
353
|
-
self.
|
|
354
|
-
set_ctx_rt_handle(self._ctx, resume_handle, self.window)
|
|
358
|
+
if ph:
|
|
359
|
+
self._persist_rt_handle(ph)
|
|
355
360
|
except Exception:
|
|
356
361
|
pass
|
|
357
362
|
|
|
@@ -360,13 +365,6 @@ class GoogleLiveClient:
|
|
|
360
365
|
apply_turn_mode_google(live_cfg, turn_mode)
|
|
361
366
|
self._tune_google_vad(live_cfg, opts)
|
|
362
367
|
|
|
363
|
-
# Save callbacks and ctx
|
|
364
|
-
self._on_text = on_text
|
|
365
|
-
self._on_audio = on_audio
|
|
366
|
-
self._should_stop = should_stop or (lambda: False)
|
|
367
|
-
self._ctx = ctx
|
|
368
|
-
self._last_opts = opts
|
|
369
|
-
|
|
370
368
|
# Control primitives
|
|
371
369
|
self._response_done = asyncio.Event()
|
|
372
370
|
self._send_lock = asyncio.Lock()
|
|
@@ -407,7 +405,7 @@ class GoogleLiveClient:
|
|
|
407
405
|
self._rt_state = None
|
|
408
406
|
self._last_tool_calls = []
|
|
409
407
|
|
|
410
|
-
# Clear
|
|
408
|
+
# Clear in-memory handle as well to prevent unintended resumption
|
|
411
409
|
self._rt_session_id = None
|
|
412
410
|
|
|
413
411
|
# Clear cached tools signature
|
|
@@ -820,11 +818,10 @@ class GoogleLiveClient:
|
|
|
820
818
|
try:
|
|
821
819
|
sru = getattr(response, "session_resumption_update", None) or getattr(response, "sessionResumptionUpdate", None)
|
|
822
820
|
if sru:
|
|
823
|
-
|
|
824
|
-
new_handle =
|
|
825
|
-
if
|
|
826
|
-
self.
|
|
827
|
-
set_ctx_rt_handle(self._ctx, self._rt_session_id, self.window)
|
|
821
|
+
# Prefer robustness: persist handle if present, regardless of 'resumable' flag inconsistencies
|
|
822
|
+
new_handle = self._extract_sru_handle(sru)
|
|
823
|
+
if isinstance(new_handle, str) and new_handle.strip():
|
|
824
|
+
self._persist_rt_handle(new_handle.strip())
|
|
828
825
|
if self.debug:
|
|
829
826
|
print(f"[google.live] session handle updated: {self._rt_session_id}")
|
|
830
827
|
except Exception:
|
|
@@ -1740,6 +1737,10 @@ class GoogleLiveClient:
|
|
|
1740
1737
|
"""
|
|
1741
1738
|
self.debug = bool(enabled)
|
|
1742
1739
|
|
|
1740
|
+
def is_session(self) -> bool:
|
|
1741
|
+
"""Check if the WS session is currently open."""
|
|
1742
|
+
return self._session is not None
|
|
1743
|
+
|
|
1743
1744
|
def is_session_active(self) -> bool:
|
|
1744
1745
|
"""Check if the WS session is currently open."""
|
|
1745
1746
|
return self._session is not None
|
|
@@ -1748,6 +1749,12 @@ class GoogleLiveClient:
|
|
|
1748
1749
|
"""Update the current CtxItem (for session handle persistence)."""
|
|
1749
1750
|
self._ctx = ctx
|
|
1750
1751
|
|
|
1752
|
+
def get_current_rt_session_id(self) -> Optional[str]:
|
|
1753
|
+
"""
|
|
1754
|
+
Return the current resumable session handle if known.
|
|
1755
|
+
"""
|
|
1756
|
+
return self._rt_session_id
|
|
1757
|
+
|
|
1751
1758
|
# -----------------------------
|
|
1752
1759
|
# Internal: auto-turn receiver bootstrap
|
|
1753
1760
|
# -----------------------------
|
|
@@ -1942,4 +1949,43 @@ class GoogleLiveClient:
|
|
|
1942
1949
|
"""
|
|
1943
1950
|
Emit RT_OUTPUT_AUDIO_COMMIT on first sign of model output in auto-turn mode.
|
|
1944
1951
|
"""
|
|
1945
|
-
self._emit_audio_commit_signal()
|
|
1952
|
+
self._emit_audio_commit_signal()
|
|
1953
|
+
|
|
1954
|
+
# -----------------------------
|
|
1955
|
+
# Internal: session handle helpers
|
|
1956
|
+
# -----------------------------
|
|
1957
|
+
|
|
1958
|
+
def _persist_rt_handle(self, handle: str) -> None:
|
|
1959
|
+
"""
|
|
1960
|
+
Persist current session handle in-memory, to ctx.extra and into last opts for future restarts.
|
|
1961
|
+
"""
|
|
1962
|
+
try:
|
|
1963
|
+
self._rt_session_id = handle
|
|
1964
|
+
set_ctx_rt_handle(self._ctx, handle, self.window)
|
|
1965
|
+
except Exception:
|
|
1966
|
+
pass
|
|
1967
|
+
try:
|
|
1968
|
+
if self._last_opts is not None:
|
|
1969
|
+
setattr(self._last_opts, "rt_session_id", handle)
|
|
1970
|
+
except Exception:
|
|
1971
|
+
pass
|
|
1972
|
+
|
|
1973
|
+
def _extract_sru_handle(self, sru: Any) -> Optional[str]:
|
|
1974
|
+
"""
|
|
1975
|
+
Extract handle from SessionResumptionUpdate (supports snake_case and camelCase, and token alias).
|
|
1976
|
+
"""
|
|
1977
|
+
# Objects (attrs)
|
|
1978
|
+
for attr in ("new_handle", "newHandle", "token"):
|
|
1979
|
+
try:
|
|
1980
|
+
v = getattr(sru, attr, None)
|
|
1981
|
+
if isinstance(v, str) and v.strip():
|
|
1982
|
+
return v.strip()
|
|
1983
|
+
except Exception:
|
|
1984
|
+
pass
|
|
1985
|
+
# Dicts
|
|
1986
|
+
if isinstance(sru, dict):
|
|
1987
|
+
for k in ("new_handle", "newHandle", "token"):
|
|
1988
|
+
v = sru.get(k)
|
|
1989
|
+
if isinstance(v, str) and v.strip():
|
|
1990
|
+
return v.strip()
|
|
1991
|
+
return None
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2026.01.
|
|
9
|
+
# Updated Date: 2026.01.07 23:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import json
|
|
@@ -86,8 +86,55 @@ class Realtime:
|
|
|
86
86
|
self.handler.send_tool_results_sync({
|
|
87
87
|
tool_call_id: tool_results
|
|
88
88
|
})
|
|
89
|
+
self.handler.update_ctx(context.ctx)
|
|
89
90
|
return True # do not start new session, just send tool results
|
|
90
91
|
|
|
92
|
+
# Tools
|
|
93
|
+
tools = self.window.core.api.google.tools.prepare(model, context.external_functions)
|
|
94
|
+
remote_tools = self.window.core.api.google.remote_tools.build_remote_tools(model)
|
|
95
|
+
if tools:
|
|
96
|
+
remote_tools = [] # in Google, remote tools are not allowed if function calling is used
|
|
97
|
+
|
|
98
|
+
# Resolve last session ID, prefer history, then fallback to current ctx and in-memory handler handle
|
|
99
|
+
last_session_id = extract_last_session_id(context.history) if context.history else None
|
|
100
|
+
if not last_session_id:
|
|
101
|
+
try:
|
|
102
|
+
if context.ctx and isinstance(context.ctx.extra, dict):
|
|
103
|
+
sid = context.ctx.extra.get("rt_session_id")
|
|
104
|
+
if isinstance(sid, str) and sid.strip():
|
|
105
|
+
last_session_id = sid.strip()
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
if not last_session_id and self.handler.is_session_active():
|
|
109
|
+
try:
|
|
110
|
+
sid = self.handler.get_current_rt_session_id()
|
|
111
|
+
if isinstance(sid, str) and sid.strip():
|
|
112
|
+
last_session_id = sid.strip()
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
if is_debug:
|
|
117
|
+
print("[realtime session] Last ID", last_session_id)
|
|
118
|
+
|
|
119
|
+
# Enforce clean state rules:
|
|
120
|
+
# - No history: always reset to ensure a fresh server context.
|
|
121
|
+
# - If history exists, keep the current live session even if the resumable handle has not been captured yet.
|
|
122
|
+
# Gemini Live can emit the handle slightly after the first turn starts; closing here would drop context continuity.
|
|
123
|
+
try:
|
|
124
|
+
history_len = len(context.history) if context.history else 0
|
|
125
|
+
except Exception:
|
|
126
|
+
history_len = 0
|
|
127
|
+
|
|
128
|
+
if history_len == 0:
|
|
129
|
+
if self.handler.is_session_active():
|
|
130
|
+
self.handler.close_session_sync()
|
|
131
|
+
try:
|
|
132
|
+
if context.ctx and isinstance(context.ctx.extra, dict):
|
|
133
|
+
context.ctx.extra.pop("rt_session_id", None)
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
last_session_id = None # force new session
|
|
137
|
+
|
|
91
138
|
# update auto-turn in active session
|
|
92
139
|
if (self.handler.is_session_active()
|
|
93
140
|
and (auto_turn != self.prev_auto_turn
|
|
@@ -95,23 +142,12 @@ class Realtime:
|
|
|
95
142
|
or opt_vad_prefix != self.prev_vad_prefix)):
|
|
96
143
|
self.handler.update_session_autoturn_sync(auto_turn, opt_vad_silence, opt_vad_prefix)
|
|
97
144
|
|
|
98
|
-
# Tools
|
|
99
|
-
tools = self.window.core.api.google.tools.prepare(model, context.external_functions)
|
|
100
|
-
remote_tools = self.window.core.api.google.remote_tools.build_remote_tools(model)
|
|
101
|
-
if tools:
|
|
102
|
-
remote_tools = [] # in Google, remote tools are not allowed if function calling is used
|
|
103
|
-
|
|
104
145
|
# if auto-turn is enabled and prompt is empty, update session and context only
|
|
105
146
|
if auto_turn and self.handler.is_session_active() and (context.prompt.strip() == "" or context.prompt == "..."):
|
|
106
147
|
self.handler.update_session_tools_sync(tools, remote_tools)
|
|
107
148
|
self.handler.update_ctx(context.ctx)
|
|
108
149
|
return True # do not send new request if session is active
|
|
109
150
|
|
|
110
|
-
# Last session ID
|
|
111
|
-
last_session_id = extract_last_session_id(context.history)
|
|
112
|
-
if is_debug:
|
|
113
|
-
print("[realtime session] Last ID", last_session_id)
|
|
114
|
-
|
|
115
151
|
# Voice
|
|
116
152
|
voice_name = "Kore"
|
|
117
153
|
try:
|
|
@@ -6,12 +6,13 @@
|
|
|
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 06:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import os
|
|
13
13
|
import time
|
|
14
|
-
|
|
14
|
+
import mimetypes
|
|
15
|
+
from typing import Optional, List, Dict, Any, Union
|
|
15
16
|
|
|
16
17
|
from pygpt_net.item.store import RemoteStoreItem
|
|
17
18
|
|
|
@@ -63,6 +64,53 @@ class Store:
|
|
|
63
64
|
v = hi
|
|
64
65
|
return v
|
|
65
66
|
|
|
67
|
+
def _download_dir(self) -> str:
|
|
68
|
+
"""
|
|
69
|
+
Resolve target download directory (uses download.dir if set).
|
|
70
|
+
"""
|
|
71
|
+
if self.window.core.config.has("download.dir") and self.window.core.config.get("download.dir") != "":
|
|
72
|
+
dir_path = os.path.join(
|
|
73
|
+
self.window.core.config.get_user_dir('data'),
|
|
74
|
+
self.window.core.config.get("download.dir"),
|
|
75
|
+
)
|
|
76
|
+
else:
|
|
77
|
+
dir_path = self.window.core.config.get_user_dir('data')
|
|
78
|
+
os.makedirs(dir_path, exist_ok=True)
|
|
79
|
+
return dir_path
|
|
80
|
+
|
|
81
|
+
def _ensure_unique_path(self, dir_path: str, filename: str) -> str:
|
|
82
|
+
"""
|
|
83
|
+
Ensure unique filename in dir, add timestamp prefix if exists.
|
|
84
|
+
"""
|
|
85
|
+
path = os.path.join(dir_path, filename)
|
|
86
|
+
if os.path.exists(path):
|
|
87
|
+
prefix = time.strftime("%Y%m%d_%H%M%S_")
|
|
88
|
+
path = os.path.join(dir_path, f"{prefix}{filename}")
|
|
89
|
+
return path
|
|
90
|
+
|
|
91
|
+
def _guess_filename(self, file_meta: Any, fallback: str = "downloaded.bin") -> str:
|
|
92
|
+
"""
|
|
93
|
+
Best-effort filename from File metadata or URI.
|
|
94
|
+
"""
|
|
95
|
+
name = None
|
|
96
|
+
for attr in ("display_name", "filename", "name", "file_name"):
|
|
97
|
+
try:
|
|
98
|
+
val = getattr(file_meta, attr, None)
|
|
99
|
+
if not name and isinstance(val, str) and val:
|
|
100
|
+
name = os.path.basename(val)
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
if not name and isinstance(file_meta, dict):
|
|
104
|
+
val = file_meta.get(attr)
|
|
105
|
+
if isinstance(val, str) and val:
|
|
106
|
+
name = os.path.basename(val)
|
|
107
|
+
|
|
108
|
+
if not name:
|
|
109
|
+
# allow URI-like strings
|
|
110
|
+
if isinstance(file_meta, str):
|
|
111
|
+
name = os.path.basename(file_meta.split("?")[0].split("#")[0])
|
|
112
|
+
return name or fallback
|
|
113
|
+
|
|
66
114
|
# -----------------------------
|
|
67
115
|
# Files service (global)
|
|
68
116
|
# -----------------------------
|
|
@@ -105,6 +153,79 @@ class Store:
|
|
|
105
153
|
if res is not None:
|
|
106
154
|
return file_name
|
|
107
155
|
|
|
156
|
+
def download(self, file: Union[str, Any], path: str) -> bool:
|
|
157
|
+
"""
|
|
158
|
+
Download a Files API item into the given path.
|
|
159
|
+
|
|
160
|
+
:param file: file name ('files/...'), file object, or file URI
|
|
161
|
+
:param path: target local path
|
|
162
|
+
:return: True on success
|
|
163
|
+
"""
|
|
164
|
+
client = self.get_client()
|
|
165
|
+
data = None
|
|
166
|
+
try:
|
|
167
|
+
data = client.files.download(file=file)
|
|
168
|
+
except Exception:
|
|
169
|
+
pass
|
|
170
|
+
if not data:
|
|
171
|
+
return False
|
|
172
|
+
# google-genai returns bytes
|
|
173
|
+
try:
|
|
174
|
+
with open(path, "wb") as f:
|
|
175
|
+
f.write(data if isinstance(data, (bytes, bytearray)) else bytes(data))
|
|
176
|
+
return True
|
|
177
|
+
except Exception:
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
def download_to_dir(self, file: Union[str, Any], prefer_name: Optional[str] = None) -> Optional[str]:
|
|
181
|
+
"""
|
|
182
|
+
Download a Files API item into configured download directory.
|
|
183
|
+
|
|
184
|
+
:param file: file name ('files/...'), file object, or file URI
|
|
185
|
+
:param prefer_name: optional preferred filename
|
|
186
|
+
:return: saved path or None
|
|
187
|
+
"""
|
|
188
|
+
dir_path = self._download_dir()
|
|
189
|
+
filename = None
|
|
190
|
+
|
|
191
|
+
# Try to resolve filename from metadata
|
|
192
|
+
file_meta = None
|
|
193
|
+
try:
|
|
194
|
+
name = None
|
|
195
|
+
if isinstance(file, str) and file.startswith("files/"):
|
|
196
|
+
name = file
|
|
197
|
+
elif hasattr(file, "name"):
|
|
198
|
+
name = getattr(file, "name", None)
|
|
199
|
+
|
|
200
|
+
if name:
|
|
201
|
+
file_meta = self.get_file(name)
|
|
202
|
+
except Exception:
|
|
203
|
+
file_meta = None
|
|
204
|
+
|
|
205
|
+
if prefer_name and isinstance(prefer_name, str):
|
|
206
|
+
filename = os.path.basename(prefer_name)
|
|
207
|
+
|
|
208
|
+
if not filename:
|
|
209
|
+
filename = self._guess_filename(file_meta if file_meta is not None else file)
|
|
210
|
+
|
|
211
|
+
# Infer extension from mime, if missing
|
|
212
|
+
if not os.path.splitext(filename)[1] and file_meta is not None:
|
|
213
|
+
try:
|
|
214
|
+
mime = getattr(file_meta, "mime_type", None)
|
|
215
|
+
if isinstance(file_meta, dict):
|
|
216
|
+
mime = file_meta.get("mime_type", mime)
|
|
217
|
+
if mime:
|
|
218
|
+
ext = mimetypes.guess_extension(mime) or ""
|
|
219
|
+
if ext:
|
|
220
|
+
filename = filename + ext
|
|
221
|
+
except Exception:
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
path = self._ensure_unique_path(dir_path, filename)
|
|
225
|
+
if self.download(file, path):
|
|
226
|
+
return path
|
|
227
|
+
return None
|
|
228
|
+
|
|
108
229
|
def get_files_ids_all(
|
|
109
230
|
self,
|
|
110
231
|
items: list,
|
|
@@ -377,7 +498,7 @@ class Store:
|
|
|
377
498
|
for doc_name in files:
|
|
378
499
|
self.log("Removing document from store [{}]:{} ".format(store_id, doc_name))
|
|
379
500
|
self.delete_store_file(store_id, doc_name)
|
|
380
|
-
num += 1
|
|
501
|
+
num += 1
|
|
381
502
|
return num
|
|
382
503
|
|
|
383
504
|
def remove_all(self, callback: Optional[callable] = None) -> int:
|
|
@@ -93,6 +93,43 @@ def process_google_chunk(ctx, core, state, chunk) -> Optional[str]:
|
|
|
93
93
|
except Exception:
|
|
94
94
|
pass
|
|
95
95
|
|
|
96
|
+
def _try_download_uri(uri: Optional[str], prefer_name: Optional[str] = None) -> Optional[str]:
|
|
97
|
+
"""
|
|
98
|
+
Attempt to download a Files API URI via store; return local path or None.
|
|
99
|
+
"""
|
|
100
|
+
if not isinstance(uri, str) or not uri:
|
|
101
|
+
return None
|
|
102
|
+
try:
|
|
103
|
+
path = core.api.google.store.download_to_dir(uri, prefer_name=prefer_name)
|
|
104
|
+
return path
|
|
105
|
+
except Exception:
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
def _append_downloaded(paths):
|
|
109
|
+
if not paths:
|
|
110
|
+
return
|
|
111
|
+
try:
|
|
112
|
+
loc = core.filesystem.make_local_list(paths)
|
|
113
|
+
except Exception:
|
|
114
|
+
loc = paths
|
|
115
|
+
if not isinstance(ctx.files, list):
|
|
116
|
+
ctx.files = []
|
|
117
|
+
for p in loc:
|
|
118
|
+
if p not in ctx.files:
|
|
119
|
+
ctx.files.append(p)
|
|
120
|
+
# images
|
|
121
|
+
imgs = []
|
|
122
|
+
for p in loc:
|
|
123
|
+
ext = p.lower().rsplit(".", 1)[-1] if "." in p else ""
|
|
124
|
+
if ext in ["png", "jpg", "jpeg", "gif", "bmp", "tiff", "webp"]:
|
|
125
|
+
imgs.append(p)
|
|
126
|
+
if imgs:
|
|
127
|
+
if not isinstance(ctx.images, list):
|
|
128
|
+
ctx.images = []
|
|
129
|
+
for p in imgs:
|
|
130
|
+
if p not in ctx.images:
|
|
131
|
+
ctx.images.append(p)
|
|
132
|
+
|
|
96
133
|
# Collect function calls from Responses API style stream
|
|
97
134
|
if fc_list:
|
|
98
135
|
for fc in fc_list:
|
|
@@ -114,6 +151,23 @@ def process_google_chunk(ctx, core, state, chunk) -> Optional[str]:
|
|
|
114
151
|
content = getattr(cand, "content", None)
|
|
115
152
|
parts = getattr(content, "parts", None) or []
|
|
116
153
|
for p in parts:
|
|
154
|
+
# Download Files API file_data parts if present
|
|
155
|
+
try:
|
|
156
|
+
fdata = getattr(p, "file_data", None)
|
|
157
|
+
if fdata:
|
|
158
|
+
uri = getattr(fdata, "file_uri", None) or getattr(fdata, "uri", None)
|
|
159
|
+
name = getattr(fdata, "file_name", None) or getattr(fdata, "display_name", None)
|
|
160
|
+
if uri and isinstance(uri, str):
|
|
161
|
+
if not hasattr(state, "google_downloaded_uris"):
|
|
162
|
+
state.google_downloaded_uris = set()
|
|
163
|
+
if uri not in state.google_downloaded_uris:
|
|
164
|
+
save = _try_download_uri(uri, name)
|
|
165
|
+
if save:
|
|
166
|
+
_append_downloaded([save])
|
|
167
|
+
state.google_downloaded_uris.add(uri)
|
|
168
|
+
except Exception:
|
|
169
|
+
pass
|
|
170
|
+
|
|
117
171
|
fn = getattr(p, "function_call", None)
|
|
118
172
|
if not fn:
|
|
119
173
|
continue
|
|
@@ -132,7 +186,6 @@ def process_google_chunk(ctx, core, state, chunk) -> Optional[str]:
|
|
|
132
186
|
pass
|
|
133
187
|
|
|
134
188
|
# Interactions API / Deep Research: collect streaming deltas and metadata
|
|
135
|
-
# Handles event_type, event_id, interaction.start/complete/status_update, and content.delta variants
|
|
136
189
|
try:
|
|
137
190
|
event_type = _get(chunk, "event_type", None)
|
|
138
191
|
if event_type:
|
|
@@ -215,10 +268,8 @@ def process_google_chunk(ctx, core, state, chunk) -> Optional[str]:
|
|
|
215
268
|
content_obj = _get(delta, "content", None)
|
|
216
269
|
thought_txt = None
|
|
217
270
|
if content_obj is not None:
|
|
218
|
-
# TextContent path
|
|
219
271
|
thought_txt = _get(content_obj, "text", None)
|
|
220
272
|
if thought_txt is None:
|
|
221
|
-
# Some SDKs expose 'thought' or 'content.text' differently
|
|
222
273
|
thought_txt = _get(delta, "thought", None)
|
|
223
274
|
if thought_txt:
|
|
224
275
|
_ensure_list_attr(state, "google_thought_summaries")
|
|
@@ -252,7 +303,6 @@ def process_google_chunk(ctx, core, state, chunk) -> Optional[str]:
|
|
|
252
303
|
|
|
253
304
|
# Function result delta (optional store)
|
|
254
305
|
elif delta_type == "function_result":
|
|
255
|
-
# Can be used to log tool results; not altering UI text
|
|
256
306
|
_ensure_list_attr(state, "google_function_results")
|
|
257
307
|
try:
|
|
258
308
|
state.google_function_results.append(_to_plain_dict(delta))
|
|
@@ -269,7 +319,6 @@ def process_google_chunk(ctx, core, state, chunk) -> Optional[str]:
|
|
|
269
319
|
else:
|
|
270
320
|
response_parts.append(str(code_txt))
|
|
271
321
|
elif delta_type == "code_execution_result":
|
|
272
|
-
# Close code block; keep output logging internal if needed
|
|
273
322
|
if state.is_code:
|
|
274
323
|
response_parts.append("\n\n```\n-----------\n")
|
|
275
324
|
state.is_code = False
|
|
@@ -282,7 +331,7 @@ def process_google_chunk(ctx, core, state, chunk) -> Optional[str]:
|
|
|
282
331
|
# Images in stream
|
|
283
332
|
elif delta_type == "image":
|
|
284
333
|
# ImageDelta may contain base64 data or uri
|
|
285
|
-
mime =
|
|
334
|
+
mime = _get(delta, "mime_type", None)
|
|
286
335
|
data_b64 = _get(delta, "data", None)
|
|
287
336
|
uri = _get(delta, "uri", None)
|
|
288
337
|
if data_b64:
|
|
@@ -299,12 +348,17 @@ def process_google_chunk(ctx, core, state, chunk) -> Optional[str]:
|
|
|
299
348
|
except Exception:
|
|
300
349
|
pass
|
|
301
350
|
elif uri:
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
351
|
+
# Try to download Files API content when URI is a file ref
|
|
352
|
+
save = _try_download_uri(uri)
|
|
353
|
+
if save:
|
|
354
|
+
_append_downloaded([save])
|
|
355
|
+
else:
|
|
356
|
+
try:
|
|
357
|
+
if not hasattr(ctx, "urls") or ctx.urls is None:
|
|
358
|
+
ctx.urls = []
|
|
359
|
+
ctx.urls.append(uri)
|
|
360
|
+
except Exception:
|
|
361
|
+
pass
|
|
308
362
|
|
|
309
363
|
# URL context call/result (Deep Research tool)
|
|
310
364
|
elif delta_type == "url_context_call":
|
|
@@ -368,7 +422,6 @@ def process_google_chunk(ctx, core, state, chunk) -> Optional[str]:
|
|
|
368
422
|
except Exception:
|
|
369
423
|
pass
|
|
370
424
|
|
|
371
|
-
# Thought signature delta (optional, store)
|
|
372
425
|
elif delta_type == "thought_signature":
|
|
373
426
|
_ensure_list_attr(state, "google_thought_signatures")
|
|
374
427
|
try:
|
|
@@ -380,12 +433,16 @@ def process_google_chunk(ctx, core, state, chunk) -> Optional[str]:
|
|
|
380
433
|
elif delta_type in ("audio", "video", "document"):
|
|
381
434
|
uri = _get(delta, "uri", None)
|
|
382
435
|
if uri:
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
436
|
+
save = _try_download_uri(uri)
|
|
437
|
+
if save:
|
|
438
|
+
_append_downloaded([save])
|
|
439
|
+
else:
|
|
440
|
+
try:
|
|
441
|
+
if not hasattr(ctx, "urls") or ctx.urls is None:
|
|
442
|
+
ctx.urls = []
|
|
443
|
+
ctx.urls.append(uri)
|
|
444
|
+
except Exception:
|
|
445
|
+
pass
|
|
389
446
|
|
|
390
447
|
except Exception:
|
|
391
448
|
pass
|
|
@@ -459,11 +516,21 @@ def process_google_chunk(ctx, core, state, chunk) -> Optional[str]:
|
|
|
459
516
|
fdata = getattr(p, "file_data", None)
|
|
460
517
|
if fdata:
|
|
461
518
|
uri = getattr(fdata, "file_uri", None) or getattr(fdata, "uri", None)
|
|
462
|
-
|
|
463
|
-
if uri
|
|
464
|
-
if
|
|
465
|
-
|
|
466
|
-
|
|
519
|
+
prefer = getattr(fdata, "file_name", None) or getattr(fdata, "display_name", None)
|
|
520
|
+
if uri:
|
|
521
|
+
if not hasattr(state, "google_downloaded_uris"):
|
|
522
|
+
state.google_downloaded_uris = set()
|
|
523
|
+
if uri not in state.google_downloaded_uris:
|
|
524
|
+
save = _try_download_uri(uri, prefer)
|
|
525
|
+
if save:
|
|
526
|
+
_append_downloaded([save])
|
|
527
|
+
state.google_downloaded_uris.add(uri)
|
|
528
|
+
# keep original behavior for image http links
|
|
529
|
+
mime = (getattr(fdata, "mime_type", "") or "").lower()
|
|
530
|
+
if uri.startswith(("http://", "https://")) and mime.startswith("image/"):
|
|
531
|
+
if ctx.urls is None:
|
|
532
|
+
ctx.urls = []
|
|
533
|
+
ctx.urls.append(uri)
|
|
467
534
|
|
|
468
535
|
collect_google_citations(ctx, state, chunk)
|
|
469
536
|
|