pygpt-net 2.7.3__py3-none-any.whl → 2.7.5__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 (157) hide show
  1. pygpt_net/CHANGELOG.txt +15 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +382 -350
  4. pygpt_net/app_core.py +4 -2
  5. pygpt_net/controller/__init__.py +5 -1
  6. pygpt_net/controller/assistant/assistant.py +1 -4
  7. pygpt_net/controller/assistant/batch.py +5 -504
  8. pygpt_net/controller/assistant/editor.py +5 -5
  9. pygpt_net/controller/assistant/files.py +16 -16
  10. pygpt_net/controller/chat/attachment.py +5 -1
  11. pygpt_net/controller/chat/handler/google_stream.py +307 -1
  12. pygpt_net/controller/chat/handler/worker.py +8 -1
  13. pygpt_net/controller/chat/image.py +15 -3
  14. pygpt_net/controller/dialogs/confirm.py +73 -101
  15. pygpt_net/controller/files/files.py +3 -1
  16. pygpt_net/controller/lang/mapping.py +9 -9
  17. pygpt_net/controller/layout/layout.py +2 -2
  18. pygpt_net/controller/painter/capture.py +50 -1
  19. pygpt_net/controller/presets/presets.py +2 -1
  20. pygpt_net/controller/remote_store/__init__.py +12 -0
  21. pygpt_net/{provider/core/assistant_file/db_sqlite → controller/remote_store/google}/__init__.py +2 -2
  22. pygpt_net/controller/remote_store/google/batch.py +402 -0
  23. pygpt_net/controller/remote_store/google/store.py +615 -0
  24. pygpt_net/controller/remote_store/openai/__init__.py +12 -0
  25. pygpt_net/controller/remote_store/openai/batch.py +524 -0
  26. pygpt_net/controller/{assistant → remote_store/openai}/store.py +63 -60
  27. pygpt_net/controller/remote_store/remote_store.py +35 -0
  28. pygpt_net/controller/theme/nodes.py +2 -1
  29. pygpt_net/controller/ui/mode.py +5 -1
  30. pygpt_net/controller/ui/ui.py +36 -2
  31. pygpt_net/core/assistants/assistants.py +3 -15
  32. pygpt_net/core/db/database.py +5 -3
  33. pygpt_net/core/filesystem/url.py +4 -1
  34. pygpt_net/core/locale/placeholder.py +35 -0
  35. pygpt_net/core/remote_store/__init__.py +12 -0
  36. pygpt_net/core/remote_store/google/__init__.py +11 -0
  37. pygpt_net/core/remote_store/google/files.py +224 -0
  38. pygpt_net/core/remote_store/google/store.py +248 -0
  39. pygpt_net/core/remote_store/openai/__init__.py +11 -0
  40. pygpt_net/core/{assistants → remote_store/openai}/files.py +26 -19
  41. pygpt_net/core/{assistants → remote_store/openai}/store.py +32 -15
  42. pygpt_net/core/remote_store/remote_store.py +24 -0
  43. pygpt_net/core/render/web/helpers.py +5 -0
  44. pygpt_net/data/config/config.json +8 -5
  45. pygpt_net/data/config/models.json +77 -3
  46. pygpt_net/data/config/settings.json +45 -14
  47. pygpt_net/data/css/web-blocks.css +3 -0
  48. pygpt_net/data/css/web-chatgpt.css +3 -0
  49. pygpt_net/data/locale/locale.de.ini +43 -41
  50. pygpt_net/data/locale/locale.en.ini +56 -44
  51. pygpt_net/data/locale/locale.es.ini +43 -41
  52. pygpt_net/data/locale/locale.fr.ini +43 -41
  53. pygpt_net/data/locale/locale.it.ini +43 -41
  54. pygpt_net/data/locale/locale.pl.ini +43 -41
  55. pygpt_net/data/locale/locale.uk.ini +43 -41
  56. pygpt_net/data/locale/locale.zh.ini +43 -41
  57. pygpt_net/data/locale/plugin.cmd_history.de.ini +1 -1
  58. pygpt_net/data/locale/plugin.cmd_history.en.ini +1 -1
  59. pygpt_net/data/locale/plugin.cmd_history.es.ini +1 -1
  60. pygpt_net/data/locale/plugin.cmd_history.fr.ini +1 -1
  61. pygpt_net/data/locale/plugin.cmd_history.it.ini +1 -1
  62. pygpt_net/data/locale/plugin.cmd_history.pl.ini +1 -1
  63. pygpt_net/data/locale/plugin.cmd_history.uk.ini +1 -1
  64. pygpt_net/data/locale/plugin.cmd_history.zh.ini +1 -1
  65. pygpt_net/data/locale/plugin.cmd_mouse_control.en.ini +14 -0
  66. pygpt_net/data/locale/plugin.cmd_web.de.ini +1 -1
  67. pygpt_net/data/locale/plugin.cmd_web.en.ini +1 -1
  68. pygpt_net/data/locale/plugin.cmd_web.es.ini +1 -1
  69. pygpt_net/data/locale/plugin.cmd_web.fr.ini +1 -1
  70. pygpt_net/data/locale/plugin.cmd_web.it.ini +1 -1
  71. pygpt_net/data/locale/plugin.cmd_web.pl.ini +1 -1
  72. pygpt_net/data/locale/plugin.cmd_web.uk.ini +1 -1
  73. pygpt_net/data/locale/plugin.cmd_web.zh.ini +1 -1
  74. pygpt_net/data/locale/plugin.idx_llama_index.de.ini +2 -2
  75. pygpt_net/data/locale/plugin.idx_llama_index.en.ini +2 -2
  76. pygpt_net/data/locale/plugin.idx_llama_index.es.ini +2 -2
  77. pygpt_net/data/locale/plugin.idx_llama_index.fr.ini +2 -2
  78. pygpt_net/data/locale/plugin.idx_llama_index.it.ini +2 -2
  79. pygpt_net/data/locale/plugin.idx_llama_index.pl.ini +2 -2
  80. pygpt_net/data/locale/plugin.idx_llama_index.uk.ini +2 -2
  81. pygpt_net/data/locale/plugin.idx_llama_index.zh.ini +2 -2
  82. pygpt_net/item/assistant.py +1 -211
  83. pygpt_net/item/ctx.py +3 -1
  84. pygpt_net/item/store.py +238 -0
  85. pygpt_net/launcher.py +115 -55
  86. pygpt_net/migrations/Version20260102190000.py +35 -0
  87. pygpt_net/migrations/__init__.py +3 -1
  88. pygpt_net/plugin/cmd_mouse_control/config.py +470 -1
  89. pygpt_net/plugin/cmd_mouse_control/plugin.py +488 -22
  90. pygpt_net/plugin/cmd_mouse_control/worker.py +464 -87
  91. pygpt_net/plugin/cmd_mouse_control/worker_sandbox.py +729 -0
  92. pygpt_net/plugin/idx_llama_index/config.py +2 -2
  93. pygpt_net/preload.py +243 -0
  94. pygpt_net/provider/api/google/__init__.py +16 -54
  95. pygpt_net/provider/api/google/chat.py +546 -129
  96. pygpt_net/provider/api/google/computer.py +190 -0
  97. pygpt_net/provider/api/google/image.py +74 -6
  98. pygpt_net/provider/api/google/realtime/realtime.py +2 -2
  99. pygpt_net/provider/api/google/remote_tools.py +93 -0
  100. pygpt_net/provider/api/google/store.py +546 -0
  101. pygpt_net/provider/api/google/video.py +9 -4
  102. pygpt_net/provider/api/google/worker/__init__.py +0 -0
  103. pygpt_net/provider/api/google/worker/importer.py +392 -0
  104. pygpt_net/provider/api/openai/computer.py +10 -1
  105. pygpt_net/provider/api/openai/image.py +42 -19
  106. pygpt_net/provider/api/openai/store.py +6 -6
  107. pygpt_net/provider/api/openai/video.py +27 -2
  108. pygpt_net/provider/api/openai/worker/importer.py +24 -24
  109. pygpt_net/provider/api/x_ai/image.py +25 -2
  110. pygpt_net/provider/core/config/patch.py +23 -1
  111. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +3 -3
  112. pygpt_net/provider/core/model/patch.py +17 -3
  113. pygpt_net/provider/core/preset/json_file.py +13 -7
  114. pygpt_net/provider/core/{assistant_file → remote_file}/__init__.py +1 -1
  115. pygpt_net/provider/core/{assistant_file → remote_file}/base.py +9 -9
  116. pygpt_net/provider/core/remote_file/db_sqlite/__init__.py +12 -0
  117. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/patch.py +1 -1
  118. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/provider.py +23 -20
  119. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/storage.py +35 -27
  120. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/utils.py +5 -4
  121. pygpt_net/provider/core/{assistant_store → remote_store}/__init__.py +1 -1
  122. pygpt_net/provider/core/{assistant_store → remote_store}/base.py +10 -10
  123. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/__init__.py +1 -1
  124. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/patch.py +1 -1
  125. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/provider.py +16 -15
  126. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/storage.py +30 -23
  127. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/utils.py +5 -4
  128. pygpt_net/provider/core/{assistant_store → remote_store}/json_file.py +9 -9
  129. pygpt_net/provider/llms/google.py +2 -2
  130. pygpt_net/ui/base/config_dialog.py +3 -2
  131. pygpt_net/ui/dialog/assistant.py +3 -3
  132. pygpt_net/ui/dialog/plugins.py +3 -1
  133. pygpt_net/ui/dialog/remote_store_google.py +539 -0
  134. pygpt_net/ui/dialog/{assistant_store.py → remote_store_openai.py} +95 -95
  135. pygpt_net/ui/dialogs.py +5 -3
  136. pygpt_net/ui/layout/chat/attachments_uploaded.py +3 -3
  137. pygpt_net/ui/layout/chat/input.py +20 -2
  138. pygpt_net/ui/layout/chat/painter.py +6 -4
  139. pygpt_net/ui/layout/toolbox/computer_env.py +26 -8
  140. pygpt_net/ui/layout/toolbox/image.py +5 -5
  141. pygpt_net/ui/layout/toolbox/video.py +5 -4
  142. pygpt_net/ui/main.py +84 -3
  143. pygpt_net/ui/menu/tools.py +13 -5
  144. pygpt_net/ui/widget/dialog/base.py +3 -10
  145. pygpt_net/ui/widget/dialog/remote_store_google.py +56 -0
  146. pygpt_net/ui/widget/dialog/{assistant_store.py → remote_store_openai.py} +9 -9
  147. pygpt_net/ui/widget/element/button.py +4 -4
  148. pygpt_net/ui/widget/lists/remote_store_google.py +248 -0
  149. pygpt_net/ui/widget/lists/{assistant_store.py → remote_store_openai.py} +21 -21
  150. pygpt_net/ui/widget/option/checkbox_list.py +47 -9
  151. pygpt_net/ui/widget/option/combo.py +158 -4
  152. pygpt_net/ui/widget/textarea/input_extra.py +664 -0
  153. {pygpt_net-2.7.3.dist-info → pygpt_net-2.7.5.dist-info}/METADATA +48 -9
  154. {pygpt_net-2.7.3.dist-info → pygpt_net-2.7.5.dist-info}/RECORD +157 -130
  155. {pygpt_net-2.7.3.dist-info → pygpt_net-2.7.5.dist-info}/LICENSE +0 -0
  156. {pygpt_net-2.7.3.dist-info → pygpt_net-2.7.5.dist-info}/WHEEL +0 -0
  157. {pygpt_net-2.7.3.dist-info → pygpt_net-2.7.5.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,190 @@
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 Szczyglinski #
9
+ # Updated Date: 2026.01.02 02:00:00 #
10
+ # ================================================== #
11
+
12
+ import json
13
+ import time
14
+ from typing import Dict, Any, List, Tuple, Optional
15
+
16
+ from google.genai import types as gtypes
17
+ from pygpt_net.item.ctx import CtxItem
18
+
19
+
20
+ class Computer:
21
+ def __init__(self, window=None):
22
+ """
23
+ Google Gemini Computer Use adapter
24
+
25
+ This adapter passes Google Gemini function calls and action events
26
+ directly to the plugin worker without translating names to legacy
27
+ commands. The workers (host and sandbox) implement the full set
28
+ of functions and handle any coordinate conversions themselves.
29
+ """
30
+ self.window = window
31
+ self._seq = 0
32
+
33
+ # --------------- Tool spec --------------- #
34
+
35
+ def get_current_env(self) -> Dict[str, Any]:
36
+ idx = self.window.ui.nodes["computer_env"].currentIndex()
37
+ return self.window.ui.nodes["computer_env"].itemData(idx)
38
+
39
+ def _map_env(self) -> gtypes.Environment:
40
+ return gtypes.Environment.ENVIRONMENT_BROWSER
41
+ env = self.get_current_env()
42
+ val = ""
43
+ if isinstance(env, str):
44
+ val = env.lower()
45
+ elif isinstance(env, dict):
46
+ val = str(env.get("value") or env.get("name") or env).lower()
47
+ if "mac" in val:
48
+ return gtypes.Environment.ENVIRONMENT_MAC
49
+ if "windows" in val or "win" in val:
50
+ return gtypes.Environment.ENVIRONMENT_WINDOWS
51
+ if "linux" in val:
52
+ return gtypes.Environment.ENVIRONMENT_LINUX
53
+ return gtypes.Environment.ENVIRONMENT_BROWSER
54
+
55
+ def get_tool(self) -> gtypes.Tool:
56
+ return gtypes.Tool(
57
+ computer_use=gtypes.ComputerUse(
58
+ environment=self._map_env(),
59
+ )
60
+ )
61
+
62
+ # --------------- Streaming handling --------------- #
63
+
64
+ def _next_id(self) -> str:
65
+ self._seq += 1
66
+ return f"gc-{int(time.time()*1000)}-{self._seq}"
67
+
68
+ def _append_call(self, tool_calls: list, id_: str, call_id: str, name: str, args: dict) -> None:
69
+ tool_calls.append({
70
+ "id": id_,
71
+ "call_id": call_id,
72
+ "type": "computer_call",
73
+ "function": {
74
+ "name": name,
75
+ "arguments": json.dumps(args or {}),
76
+ }
77
+ })
78
+
79
+ def _append_screenshot(self, tool_calls: list, id_: str, call_id: str) -> None:
80
+ self._append_call(tool_calls, id_, call_id, "get_screenshot", {})
81
+
82
+ def _record_pending_checks(self, ctx: CtxItem, pending: Optional[list]) -> None:
83
+ if not pending:
84
+ return
85
+ ctx.extra["pending_safety_checks"] = []
86
+ for item in pending:
87
+ try:
88
+ ctx.extra["pending_safety_checks"].append({
89
+ "id": getattr(item, "id", None),
90
+ "code": getattr(item, "code", None),
91
+ "message": getattr(item, "message", None),
92
+ })
93
+ except Exception:
94
+ pass
95
+
96
+ def handle_stream_chunk(self, ctx: CtxItem, chunk, tool_calls: list) -> Tuple[List, bool]:
97
+ """
98
+ Handle function_call parts (Gemini) and older action-shaped events.
99
+ All functions are passed through unchanged to the worker.
100
+
101
+ Returns: updated tool_calls and a boolean indicating if there were calls.
102
+ """
103
+ has_calls = False
104
+
105
+ # Case A: Google SDK function_call parts (recommended)
106
+ for fname, fargs in self._iter_function_calls(chunk):
107
+ if not fname:
108
+ continue
109
+ id_ = self._next_id()
110
+ call_id = id_
111
+ try:
112
+ self._append_call(tool_calls, id_, call_id, fname, fargs or {})
113
+ has_calls = True
114
+ except Exception as e:
115
+ print(f"Gemini pass-through error for function '{fname}': {e}")
116
+
117
+ # Case B: Older/OpenAI-shaped events embedded as chunk.item.action
118
+ try:
119
+ item = getattr(chunk, "item", None)
120
+ if item and getattr(item, "type", "") == "computer_call":
121
+ id_ = getattr(item, "id", None) or self._next_id()
122
+ call_id = getattr(item, "call_id", None) or id_
123
+ action = getattr(item, "action", None)
124
+ if action:
125
+ name, args = self._pass_action(action)
126
+ if name:
127
+ self._append_call(tool_calls, id_, call_id, name, args)
128
+ has_calls = True
129
+ # optional pending safety checks
130
+ if getattr(item, "pending_safety_checks", None):
131
+ self._record_pending_checks(ctx, item.pending_safety_checks)
132
+ except Exception as e:
133
+ print(f"Gemini action stream parse error: {e}")
134
+
135
+ return tool_calls, has_calls
136
+
137
+ # --------------- Parsers --------------- #
138
+
139
+ def _iter_function_calls(self, resp) -> List[tuple]:
140
+ calls = []
141
+ try:
142
+ candidates = getattr(resp, "candidates", None)
143
+ if candidates:
144
+ for cand in candidates:
145
+ content = getattr(cand, "content", None)
146
+ if content:
147
+ parts = getattr(content, "parts", None)
148
+ if parts:
149
+ for part in parts:
150
+ fc = getattr(part, "function_call", None)
151
+ if fc:
152
+ name = getattr(fc, "name", None)
153
+ args = getattr(fc, "args", {}) or {}
154
+ calls.append((name, args))
155
+ else:
156
+ if isinstance(resp, dict):
157
+ content = resp.get("content", {})
158
+ parts = content.get("parts", [])
159
+ for part in parts:
160
+ if "function_call" in part:
161
+ fc = part["function_call"]
162
+ calls.append((fc.get("name"), fc.get("args", {})))
163
+ except Exception as e:
164
+ print(f"Gemini: failed to parse function_call: {e}")
165
+ return calls
166
+
167
+ def _pass_action(self, action) -> Tuple[Optional[str], dict]:
168
+ """
169
+ Convert old-style action object into a direct function call name + args,
170
+ without changing the semantic names (workers handle details).
171
+ """
172
+ try:
173
+ atype = getattr(action, "type", None)
174
+ if not atype:
175
+ return None, {}
176
+ atype = str(atype)
177
+
178
+ # Build args by introspection; workers know how to interpret them
179
+ args = {}
180
+ for attr in ("x", "y", "button", "scroll_x", "scroll_y", "keys", "text", "path"):
181
+ if hasattr(action, attr):
182
+ args[attr] = getattr(action, attr)
183
+
184
+ if atype == "double_click":
185
+ args["num_clicks"] = 2
186
+
187
+ return atype, args
188
+ except Exception as e:
189
+ print(f"Gemini: pass_action error: {e}")
190
+ 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: 2025.12.30 22:00:00 #
9
+ # Updated Date: 2025.12.31 16:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import mimetypes
@@ -55,6 +55,7 @@ class Image:
55
55
  prompt = context.prompt
56
56
  num = int(extra.get("num", 1))
57
57
  inline = bool(extra.get("inline", False))
58
+ extra_prompt = extra.get("extra_prompt", "")
58
59
 
59
60
  # decide sub-mode based on attachments
60
61
  sub_mode = self.MODE_GENERATE
@@ -79,6 +80,7 @@ class Image:
79
80
  worker.raw = self.window.core.config.get('img_raw')
80
81
  worker.num = num
81
82
  worker.inline = inline
83
+ worker.extra_prompt = extra_prompt
82
84
 
83
85
  # remix: previous image reference (ID/URI/path) from extra
84
86
  worker.image_id = extra.get("image_id")
@@ -129,6 +131,7 @@ class ImageWorker(QRunnable):
129
131
  self.input_prompt = ""
130
132
  self.system_prompt = ""
131
133
  self.inline = False
134
+ self.extra_prompt: Optional[str] = None
132
135
  self.raw = False
133
136
  self.num = 1
134
137
  self.resolution = "1024x1024" # used to derive aspect ratio or image_size
@@ -178,6 +181,18 @@ class ImageWorker(QRunnable):
178
181
  self.signals.error.emit(e)
179
182
  self.signals.status.emit(trans('img.status.prompt.error') + ": " + str(e))
180
183
 
184
+ # Decide how to apply negative prompt: native param on Vertex Imagen 3.0 (-001) or inline fallback.
185
+ use_param = (
186
+ bool(self.extra_prompt and str(self.extra_prompt).strip())
187
+ and self._using_vertex()
188
+ and self._imagen_supports_negative_prompt(self.model)
189
+ )
190
+ if (self.extra_prompt and str(self.extra_prompt).strip()) and not use_param:
191
+ try:
192
+ self.input_prompt = self._merge_negative_prompt(self.input_prompt or "", self.extra_prompt)
193
+ except Exception:
194
+ pass
195
+
181
196
  paths: List[str] = []
182
197
 
183
198
  # Remix path: if image_id provided, prefer image-to-image remix using the given identifier.
@@ -198,11 +213,21 @@ class ImageWorker(QRunnable):
198
213
  mask_dilation=0.0,
199
214
  ),
200
215
  )
201
- cfg = gtypes.EditImageConfig(
216
+ # Prepare edit config with optional negative prompt when supported
217
+ cfg_kwargs = dict(
202
218
  edit_mode="EDIT_MODE_DEFAULT",
203
219
  number_of_images=min(self.num, self.imagen_max_num),
204
220
  include_rai_reason=True,
205
221
  )
222
+ if self.extra_prompt and self._imagen_supports_negative_prompt(self.model):
223
+ cfg_kwargs["negative_prompt"] = self.extra_prompt
224
+ try:
225
+ cfg = gtypes.EditImageConfig(**cfg_kwargs)
226
+ except Exception:
227
+ # Fallback without negative_prompt if SDK doesn't recognize it
228
+ cfg_kwargs.pop("negative_prompt", None)
229
+ cfg = gtypes.EditImageConfig(**cfg_kwargs)
230
+
206
231
  resp = self.client.models.edit_image(
207
232
  model="imagen-3.0-capability-001",
208
233
  prompt=self.input_prompt or "",
@@ -355,12 +380,34 @@ class ImageWorker(QRunnable):
355
380
  mid = str(model_id).lower()
356
381
  return "imagen" in mid and "generate" in mid
357
382
 
383
+ def _imagen_supports_negative_prompt(self, model_id: str) -> bool:
384
+ """
385
+ Return True if the Imagen model supports native negative_prompt.
386
+ Supported: imagen-3.0-generate-001, imagen-3.0-fast-generate-001, imagen-3.0-capability-001.
387
+ """
388
+ mid = str(model_id or "").lower()
389
+ return any(x in mid for x in (
390
+ "imagen-3.0-generate-001",
391
+ "imagen-3.0-fast-generate-001",
392
+ "imagen-3.0-capability-001",
393
+ ))
394
+
358
395
  def _imagen_generate(self, prompt: str, num: int, resolution: str):
359
396
  """Imagen text-to-image."""
360
397
  aspect = self._aspect_from_resolution(resolution)
361
- cfg = gtypes.GenerateImagesConfig(number_of_images=num)
398
+ # Build config with optional negative_prompt when supported by model and provided.
399
+ cfg_kwargs: Dict[str, Any] = {"number_of_images": num}
362
400
  if aspect:
363
- cfg.aspect_ratio = aspect
401
+ cfg_kwargs["aspect_ratio"] = aspect
402
+ if self.extra_prompt and self._imagen_supports_negative_prompt(self.model):
403
+ cfg_kwargs["negative_prompt"] = self.extra_prompt
404
+ try:
405
+ cfg = gtypes.GenerateImagesConfig(**cfg_kwargs)
406
+ except Exception:
407
+ # Fallback without negative_prompt if SDK doesn't recognize it
408
+ cfg_kwargs.pop("negative_prompt", None)
409
+ cfg = gtypes.GenerateImagesConfig(**cfg_kwargs)
410
+
364
411
  return self.client.models.generate_images(
365
412
  model=self.model,
366
413
  prompt=prompt,
@@ -401,11 +448,19 @@ class ImageWorker(QRunnable):
401
448
  )
402
449
  edit_mode = "EDIT_MODE_BGSWAP"
403
450
 
404
- cfg = gtypes.EditImageConfig(
451
+ # Build edit config with optional negative_prompt
452
+ cfg_kwargs = dict(
405
453
  edit_mode=edit_mode,
406
454
  number_of_images=min(num, self.imagen_max_num),
407
455
  include_rai_reason=True,
408
456
  )
457
+ if self.extra_prompt and self._imagen_supports_negative_prompt(self.model):
458
+ cfg_kwargs["negative_prompt"] = self.extra_prompt
459
+ try:
460
+ cfg = gtypes.EditImageConfig(**cfg_kwargs)
461
+ except Exception:
462
+ cfg_kwargs.pop("negative_prompt", None)
463
+ cfg = gtypes.EditImageConfig(**cfg_kwargs)
409
464
 
410
465
  # Ensure capability model for edit
411
466
  model_id = "imagen-3.0-capability-001"
@@ -806,4 +861,17 @@ class ImageWorker(QRunnable):
806
861
  try:
807
862
  sig.deleteLater()
808
863
  except RuntimeError:
809
- pass
864
+ pass
865
+
866
+ # ---------- prompt utilities ----------
867
+
868
+ @staticmethod
869
+ def _merge_negative_prompt(prompt: str, negative: Optional[str]) -> str:
870
+ """
871
+ Append a negative prompt to the main text prompt when the provider has no native negative_prompt field.
872
+ """
873
+ base = (prompt or "").strip()
874
+ neg = (negative or "").strip()
875
+ if not neg:
876
+ return base
877
+ return (base + ("\n" if base else "") + f"Negative prompt: {neg}").strip()
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.31 23:00:00 #
9
+ # Updated Date: 2026.01.02 19:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import json
@@ -97,7 +97,7 @@ class Realtime:
97
97
 
98
98
  # Tools
99
99
  tools = self.window.core.api.google.tools.prepare(model, context.external_functions)
100
- remote_tools = self.window.core.api.google.build_remote_tools(model)
100
+ remote_tools = self.window.core.api.google.remote_tools.build_remote_tools(model)
101
101
  if tools:
102
102
  remote_tools = [] # in Google, remote tools are not allowed if function calling is used
103
103
 
@@ -0,0 +1,93 @@
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.02 19:00:00 #
10
+ # ================================================== #
11
+
12
+ from google.genai import types as gtypes
13
+
14
+ from pygpt_net.item.model import ModelItem
15
+
16
+
17
+ class RemoteTools:
18
+ def __init__(self, window=None):
19
+ """
20
+ Remote Tools helpers for Google GenAI.
21
+
22
+ :param window: Window instance
23
+ """
24
+ self.window = window
25
+
26
+ def build_remote_tools(self, model: ModelItem = None) -> list:
27
+ """
28
+ Build Google GenAI remote tools based on config flags.
29
+ - remote_tools.google.web_search: enables grounding via Google Search (Gemini 2.x)
30
+ or GoogleSearchRetrieval (Gemini 1.5 fallback).
31
+ - remote_tools.google.code_interpreter: enables code execution tool.
32
+
33
+ Returns a list of gtypes.Tool objects (can be empty).
34
+
35
+ :param model: ModelItem
36
+ :return: list of gtypes.Tool
37
+ """
38
+ tools: list = []
39
+ cfg = self.window.core.config
40
+ model_id = (model.id if model and getattr(model, "id", None) else "").lower()
41
+ is_web = self.window.controller.chat.remote_tools.enabled(model, "web_search") # get global config
42
+
43
+ # Google Search tool
44
+ if is_web and "image" not in model.id:
45
+ try:
46
+ if not model_id.startswith("gemini-1.5") and not model_id.startswith("models/gemini-1.5"):
47
+ # Gemini 2.x uses GoogleSearch
48
+ tools.append(gtypes.Tool(google_search=gtypes.GoogleSearch()))
49
+ else:
50
+ # Gemini 1.5 fallback uses GoogleSearchRetrieval
51
+ # Note: Supported only for 1.5 models.
52
+ tools.append(gtypes.Tool(
53
+ google_search_retrieval=gtypes.GoogleSearchRetrieval()
54
+ ))
55
+ except Exception as e:
56
+ # Do not break the request if tool construction fails
57
+ self.window.core.debug.log(e)
58
+
59
+ # Code Execution tool
60
+ if cfg.get("remote_tools.google.code_interpreter") and "image" not in model.id:
61
+ try:
62
+ tools.append(gtypes.Tool(code_execution=gtypes.ToolCodeExecution))
63
+ except Exception as e:
64
+ self.window.core.debug.log(e)
65
+
66
+ # URL Context tool
67
+ if cfg.get("remote_tools.google.url_ctx") and "image" not in model.id:
68
+ try:
69
+ # Supported on Gemini 2.x+ models (not on 1.5)
70
+ if not model_id.startswith("gemini-1.5") and not model_id.startswith("models/gemini-1.5"):
71
+ tools.append(gtypes.Tool(url_context=gtypes.UrlContext))
72
+ except Exception as e:
73
+ self.window.core.debug.log(e)
74
+
75
+ # Google Maps
76
+ if cfg.get("remote_tools.google.maps") and "image" not in model.id:
77
+ try:
78
+ tools.append(gtypes.Tool(google_maps=gtypes.GoogleMaps()))
79
+ except Exception as e:
80
+ self.window.core.debug.log(e)
81
+
82
+ # File search
83
+ if cfg.get("remote_tools.google.file_search") and "image" not in model.id:
84
+ store_ids = cfg.get("remote_tools.google.file_search.args", "")
85
+ file_search_store_names = [s.strip() for s in store_ids.split(",") if s.strip()]
86
+ try:
87
+ tools.append(gtypes.Tool(file_search=gtypes.FileSearch(
88
+ file_search_store_names=file_search_store_names,
89
+ )))
90
+ except Exception as e:
91
+ self.window.core.debug.log(e)
92
+
93
+ return tools