pygpt-net 2.6.64__py3-none-any.whl → 2.6.66__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 (68) hide show
  1. pygpt_net/CHANGELOG.txt +21 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +5 -1
  4. pygpt_net/controller/chat/chat.py +0 -0
  5. pygpt_net/controller/chat/handler/openai_stream.py +137 -7
  6. pygpt_net/controller/chat/render.py +0 -0
  7. pygpt_net/controller/config/field/checkbox_list.py +34 -1
  8. pygpt_net/controller/files/files.py +71 -2
  9. pygpt_net/controller/media/media.py +20 -1
  10. pygpt_net/controller/presets/editor.py +137 -22
  11. pygpt_net/controller/presets/presets.py +4 -1
  12. pygpt_net/controller/ui/mode.py +14 -10
  13. pygpt_net/controller/ui/ui.py +18 -1
  14. pygpt_net/core/agents/custom/__init__.py +18 -2
  15. pygpt_net/core/agents/custom/runner.py +2 -2
  16. pygpt_net/core/attachments/clipboard.py +146 -0
  17. pygpt_net/core/image/image.py +34 -1
  18. pygpt_net/core/render/web/renderer.py +33 -11
  19. pygpt_net/core/tabs/tabs.py +0 -0
  20. pygpt_net/core/types/image.py +61 -3
  21. pygpt_net/data/config/config.json +4 -3
  22. pygpt_net/data/config/models.json +629 -41
  23. pygpt_net/data/css/style.dark.css +12 -0
  24. pygpt_net/data/css/style.light.css +12 -0
  25. pygpt_net/data/icons/pin2.svg +1 -0
  26. pygpt_net/data/icons/pin3.svg +3 -0
  27. pygpt_net/data/icons/point.svg +1 -0
  28. pygpt_net/data/icons/target.svg +1 -0
  29. pygpt_net/data/js/app/ui.js +19 -2
  30. pygpt_net/data/js/app/user.js +22 -54
  31. pygpt_net/data/js/app.min.js +7 -9
  32. pygpt_net/data/locale/locale.de.ini +4 -0
  33. pygpt_net/data/locale/locale.en.ini +8 -0
  34. pygpt_net/data/locale/locale.es.ini +4 -0
  35. pygpt_net/data/locale/locale.fr.ini +4 -0
  36. pygpt_net/data/locale/locale.it.ini +4 -0
  37. pygpt_net/data/locale/locale.pl.ini +4 -0
  38. pygpt_net/data/locale/locale.uk.ini +4 -0
  39. pygpt_net/data/locale/locale.zh.ini +4 -0
  40. pygpt_net/icons.qrc +4 -0
  41. pygpt_net/icons_rc.py +274 -137
  42. pygpt_net/item/model.py +15 -19
  43. pygpt_net/js_rc.py +2038 -2075
  44. pygpt_net/provider/agents/openai/agent.py +0 -0
  45. pygpt_net/provider/api/google/__init__.py +20 -9
  46. pygpt_net/provider/api/google/image.py +161 -28
  47. pygpt_net/provider/api/google/video.py +73 -36
  48. pygpt_net/provider/api/openai/__init__.py +21 -11
  49. pygpt_net/provider/api/openai/agents/client.py +0 -0
  50. pygpt_net/provider/api/openai/video.py +562 -0
  51. pygpt_net/provider/core/config/patch.py +15 -0
  52. pygpt_net/provider/core/model/patch.py +29 -3
  53. pygpt_net/provider/vector_stores/qdrant.py +117 -0
  54. pygpt_net/ui/__init__.py +6 -1
  55. pygpt_net/ui/dialog/preset.py +9 -4
  56. pygpt_net/ui/layout/chat/attachments.py +18 -1
  57. pygpt_net/ui/layout/status.py +3 -3
  58. pygpt_net/ui/layout/toolbox/raw.py +7 -1
  59. pygpt_net/ui/widget/element/status.py +55 -0
  60. pygpt_net/ui/widget/filesystem/explorer.py +116 -2
  61. pygpt_net/ui/widget/lists/context.py +26 -16
  62. pygpt_net/ui/widget/option/checkbox_list.py +14 -2
  63. pygpt_net/ui/widget/textarea/input.py +71 -17
  64. {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.66.dist-info}/METADATA +76 -25
  65. {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.66.dist-info}/RECORD +63 -55
  66. {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.66.dist-info}/LICENSE +0 -0
  67. {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.66.dist-info}/WHEEL +0 -0
  68. {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.66.dist-info}/entry_points.txt +0 -0
pygpt_net/CHANGELOG.txt CHANGED
@@ -1,3 +1,24 @@
1
+ 2.6.66 (2025-12-25)
2
+
3
+ - Added Sora 2 support - #155.
4
+ - Added Nano Banana support.
5
+ - Added Qdrant Vector Store - merged PR #147 by @Anush008.
6
+ - Added models: gpt-5.2, gpt-image-1.5, gemini-3, nano-banana-pro, sora-2, claude-sonnet-4.5, claude-opus-4.5, veo-3.1.
7
+ - Added Select/unselect All option in checkbox lists.
8
+ - OpenAI SDK upgraded to 2.14.0, Anthropic SDK upgraded to 0.75.0, xAI SDK upgraded to 1.5.0, Google GenAI upgraded to 1.56.0, LlamaIndex upgraded to 0.14.10.
9
+ - Fix: charset-normalizer 3.2.0 circular import - #152.
10
+ - Fix: Google client closed state.
11
+
12
+ 2.6.65 (2025-09-28)
13
+
14
+ - Added drag and drop functionality for files and directories from the filesystem in attachments and file explorer.
15
+ - Added automatic thumbnail generation when uploading avatars.
16
+ - Added a last status timer.
17
+ - Added a fade effect to collapsed user messages.
18
+ - Added a scroll area to the agent options in the presets editor.
19
+ - Added a hover effect to lists.
20
+ - Improved UI/UX.
21
+
1
22
  2.6.64 (2025-09-27)
2
23
 
3
24
  - Added translations to agent headers.
pygpt_net/__init__.py CHANGED
@@ -6,15 +6,15 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.09.27 00:00:00 #
9
+ # Updated Date: 2025.12.15 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  __author__ = "Marcin Szczygliński"
13
13
  __copyright__ = "Copyright 2025, Marcin Szczygliński"
14
14
  __credits__ = ["Marcin Szczygliński"]
15
15
  __license__ = "MIT"
16
- __version__ = "2.6.64"
17
- __build__ = "2025-09-27"
16
+ __version__ = "2.6.66"
17
+ __build__ = "2025-12-25"
18
18
  __maintainer__ = "Marcin Szczygliński"
19
19
  __github__ = "https://github.com/szczyglis-dev/py-gpt"
20
20
  __report__ = "https://github.com/szczyglis-dev/py-gpt/issues"
pygpt_net/app.py CHANGED
@@ -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.09.22 19:00:00 #
9
+ # Updated Date: 2025.09.28 09:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -20,6 +20,8 @@ from pygpt_net.utils import set_env
20
20
 
21
21
  # app env
22
22
  set_env("PYGPT_APP_ENV", "prod", allow_overwrite=True) # dev | prod
23
+ # IF dev, JS will be loaded from `data/js/app/*` [js_rc.py], not from `data/js/app.min.js`
24
+ # recompile js_rc.py with: bin/resources.sh, minify to app.min.js with: bin/minify.sh
23
25
 
24
26
  # debug
25
27
  # set_env("QTWEBENGINE_REMOTE_DEBUGGING", 9222)
@@ -144,6 +146,7 @@ from pygpt_net.provider.llms.open_router import OpenRouterLLM
144
146
  from pygpt_net.provider.vector_stores.chroma import ChromaProvider
145
147
  from pygpt_net.provider.vector_stores.elasticsearch import ElasticsearchProvider
146
148
  from pygpt_net.provider.vector_stores.pinecode import PinecodeProvider
149
+ from pygpt_net.provider.vector_stores.qdrant import QdrantProvider
147
150
  from pygpt_net.provider.vector_stores.redis import RedisProvider
148
151
  from pygpt_net.provider.vector_stores.simple import SimpleProvider
149
152
 
@@ -473,6 +476,7 @@ def run(**kwargs):
473
476
  launcher.add_vector_store(ChromaProvider())
474
477
  launcher.add_vector_store(ElasticsearchProvider())
475
478
  launcher.add_vector_store(PinecodeProvider())
479
+ launcher.add_vector_store(QdrantProvider())
476
480
  launcher.add_vector_store(RedisProvider())
477
481
  launcher.add_vector_store(SimpleProvider())
478
482
 
File without changes
@@ -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.09.05 00:00:00 #
9
+ # Updated Date: 2025.12.26 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import base64
@@ -17,6 +17,115 @@ from typing import Optional, Any
17
17
  from .utils import capture_openai_usage
18
18
 
19
19
 
20
+ # v2: Support both dict and Pydantic objects returned by OpenAI Python SDK v2
21
+ def _to_dict_safe(obj: Any) -> Optional[dict]:
22
+ """
23
+ Convert OpenAI SDK typed models (Pydantic) or plain objects to dict safely.
24
+
25
+ Returns:
26
+ dict or None
27
+ """
28
+ if obj is None:
29
+ return None
30
+ if isinstance(obj, dict):
31
+ return obj
32
+ # Pydantic v2
33
+ try:
34
+ if hasattr(obj, "model_dump"):
35
+ return obj.model_dump()
36
+ except Exception:
37
+ pass
38
+ # Pydantic v1 fallback
39
+ try:
40
+ if hasattr(obj, "dict"):
41
+ return obj.dict()
42
+ except Exception:
43
+ pass
44
+ # Generic best-effort
45
+ try:
46
+ return dict(obj)
47
+ except Exception:
48
+ pass
49
+ try:
50
+ return getattr(obj, "__dict__", None)
51
+ except Exception:
52
+ pass
53
+ return None
54
+
55
+
56
+ # v2: Extract nested attribute or dict key chain (e.g. "url_citation.url")
57
+ def _deep_get(obj: Any, path: str, default: Any = None) -> Any:
58
+ """
59
+ Best-effort nested getter that works for dicts and objects.
60
+ """
61
+ cur = obj
62
+ for part in path.split("."):
63
+ if cur is None:
64
+ return default
65
+ if isinstance(cur, dict):
66
+ cur = cur.get(part, None)
67
+ else:
68
+ cur = getattr(cur, part, None)
69
+ return cur if cur is not None else default
70
+
71
+
72
+ # v2: Normalize annotation shape across SDK versions
73
+ def _annotation_type(ann: Any) -> Optional[str]:
74
+ """
75
+ Return the annotation 'type' in a robust way.
76
+ """
77
+ t = getattr(ann, "type", None)
78
+ if t:
79
+ return t
80
+ if isinstance(ann, dict):
81
+ return ann.get("type")
82
+ # Try dictified view
83
+ ann_d = _to_dict_safe(ann)
84
+ if isinstance(ann_d, dict):
85
+ return ann_d.get("type")
86
+ return None
87
+
88
+
89
+ # v2: Extract URL from url_citation annotation across shapes
90
+ def _extract_url_from_annotation(ann: Any) -> Optional[str]:
91
+ """
92
+ Supports shapes:
93
+ - {"type":"url_citation","url":"..."}
94
+ - {"type":"url_citation","url_citation":{"url":"..."}}
95
+ - Typed models with attributes: ann.url OR ann.url_citation.url
96
+ """
97
+ # direct attribute
98
+ url = getattr(ann, "url", None)
99
+ if isinstance(url, str) and url:
100
+ return url
101
+
102
+ # dict direct
103
+ if isinstance(ann, dict):
104
+ url = ann.get("url")
105
+ if isinstance(url, str) and url:
106
+ return url
107
+ nested = ann.get("url_citation")
108
+ if isinstance(nested, dict):
109
+ url = nested.get("url")
110
+ if isinstance(url, str) and url:
111
+ return url
112
+
113
+ # typed nested or generic deep getters
114
+ for candidate in ("url_citation.url", "url_citation.href", "href", "source_url"):
115
+ url = _deep_get(ann, candidate)
116
+ if isinstance(url, str) and url:
117
+ return url
118
+
119
+ # try after dictify
120
+ ann_d = _to_dict_safe(ann)
121
+ if isinstance(ann_d, dict):
122
+ url = ann_d.get("url") or _deep_get(ann_d, "url_citation.url")
123
+ if isinstance(url, str) and url:
124
+ return url
125
+
126
+ return None
127
+
128
+
20
129
  def process_api_chat(ctx, state, chunk) -> Optional[str]:
21
130
  """
22
131
  OpenAI-compatible Chat Completions stream delta (robust to dict/object tool_calls).
@@ -196,17 +305,38 @@ def process_api_chat_responses(ctx, core, state, chunk, etype: Optional[str]) ->
196
305
 
197
306
  elif etype == "response.output_text.annotation.added":
198
307
  ann = chunk.annotation
199
- if ann['type'] == "url_citation":
308
+
309
+ # v2: SDK v2 can return a typed model; support both dict and typed
310
+ a_type = _annotation_type(ann)
311
+
312
+ if a_type == "url_citation":
200
313
  if state.citations is None:
201
314
  state.citations = []
202
- url_citation = ann['url']
203
- if url_citation not in state.citations:
315
+
316
+ # Extract URL across shapes and SDK versions
317
+ url_citation = _extract_url_from_annotation(ann)
318
+
319
+ if url_citation and url_citation not in state.citations:
204
320
  state.citations.append(url_citation)
321
+
322
+ # keep ctx.urls always reflecting the current list
205
323
  ctx.urls = state.citations
206
- elif ann['type'] == "container_file_citation":
324
+
325
+ elif a_type == "container_file_citation":
326
+ # container-created file (Code Interpreter)
327
+ ann_d = _to_dict_safe(ann) or {}
328
+ state.files.append({
329
+ "container_id": ann_d.get("container_id", _deep_get(ann, "container_id")),
330
+ "file_id": ann_d.get("file_id", _deep_get(ann, "file_id")),
331
+ })
332
+
333
+ elif a_type == "file_citation":
334
+ # v2: Some SDKs emit plain 'file_citation' (non-container). Keep parity with container handling.
335
+ ann_d = _to_dict_safe(ann) or {}
336
+ # optional: store as generic file citation (without container)
207
337
  state.files.append({
208
- "container_id": ann['container_id'],
209
- "file_id": ann['file_id'],
338
+ "container_id": ann_d.get("container_id", _deep_get(ann, "container_id")), # may be None
339
+ "file_id": ann_d.get("file_id", _deep_get(ann, "file_id")),
210
340
  })
211
341
 
212
342
  elif etype == "response.reasoning_summary_text.delta":
File without changes
@@ -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.15 23:00:00 #
9
+ # Updated Date: 2025.12.25 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Any, Dict, List
@@ -86,6 +86,39 @@ class CheckboxList:
86
86
  except Exception as e:
87
87
  self.window.core.debug.log(e)
88
88
 
89
+
90
+ def on_select_all(
91
+ self,
92
+ parent_id: str,
93
+ key: str,
94
+ option: dict
95
+ ):
96
+ """
97
+ Event: select all checkboxes
98
+
99
+ :param parent_id: Options parent ID
100
+ :param key: Option key
101
+ :param option: Option data
102
+ """
103
+ ui = self.window.ui
104
+ cfg_parent = ui.config.get(parent_id)
105
+ if not cfg_parent:
106
+ return
107
+ entry = cfg_parent.get(key)
108
+ if entry is None or not hasattr(entry, "boxes"):
109
+ return
110
+ boxes = entry.boxes
111
+
112
+ mode = "unselect_all"
113
+
114
+ for name, cb in boxes.items():
115
+ if cb is not None and not cb.isChecked():
116
+ mode = "select_all"
117
+
118
+ for name, cb in boxes.items():
119
+ if cb is not None:
120
+ cb.setChecked(mode == "select_all")
121
+
89
122
  def get_value(
90
123
  self,
91
124
  parent_id: str,
@@ -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.25 18:00:00 #
9
+ # Updated Date: 2025.09.28 08:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import datetime
@@ -270,6 +270,75 @@ class Files:
270
270
  self.window.update_status(f"[OK] Uploaded: {num} files.")
271
271
  self.update_explorer()
272
272
 
273
+ def upload_paths(
274
+ self,
275
+ paths: list,
276
+ target_directory: Optional[str] = None
277
+ ):
278
+ """
279
+ Upload provided local paths (files or directories) into target directory.
280
+ - Directories are copied recursively.
281
+ - Name collisions are resolved using timestamp prefix, consistent with upload_local().
282
+ - Skips copying directory into itself or its subdirectory.
283
+
284
+ :param paths: list of absolute local paths
285
+ :param target_directory: destination directory (defaults to user 'data' dir)
286
+ """
287
+ if not paths:
288
+ return
289
+ if target_directory is None:
290
+ target_directory = self.window.core.config.get_user_dir('data')
291
+
292
+ try:
293
+ if not os.path.exists(target_directory):
294
+ os.makedirs(target_directory, exist_ok=True)
295
+ except Exception as e:
296
+ self.window.core.debug.log(e)
297
+ return
298
+
299
+ copied = 0
300
+
301
+ def unique_dest(dest_path: str) -> str:
302
+ if not os.path.exists(dest_path):
303
+ return dest_path
304
+ base_dir = os.path.dirname(dest_path)
305
+ name = os.path.basename(dest_path)
306
+ new_name = self.make_ts_prefix() + "_" + name
307
+ return os.path.join(base_dir, new_name)
308
+
309
+ for src in paths:
310
+ try:
311
+ if not src or not os.path.exists(src):
312
+ continue
313
+
314
+ # Prevent copying a directory into itself or its subdirectory
315
+ try:
316
+ if os.path.isdir(src):
317
+ common = os.path.commonpath([os.path.abspath(src), os.path.abspath(target_directory)])
318
+ if common == os.path.abspath(src):
319
+ # target is inside src; skip
320
+ self.window.core.debug.log(f"Skipped copying directory into itself: {src} -> {target_directory}")
321
+ continue
322
+ except Exception:
323
+ pass
324
+
325
+ dest_base = os.path.join(target_directory, os.path.basename(src))
326
+ dest_path = unique_dest(dest_base)
327
+
328
+ if os.path.isdir(src):
329
+ shutil.copytree(src, dest_path)
330
+ copied += 1
331
+ else:
332
+ copy2(src, dest_path)
333
+ copied += 1
334
+ except Exception as e:
335
+ self.window.core.debug.log(e)
336
+ print(f"Error uploading path {src}: {e}")
337
+
338
+ if copied > 0:
339
+ self.window.update_status(f"[OK] Uploaded: {copied} files.")
340
+ self.update_explorer()
341
+
273
342
  def rename(self, path: str):
274
343
  """
275
344
  Rename file or directory
@@ -480,4 +549,4 @@ class Files:
480
549
 
481
550
  def reload(self):
482
551
  """Reload files"""
483
- self.update_explorer(reload=True)
552
+ self.update_explorer(reload=True)
@@ -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.09.01 23:00:00 #
9
+ # Updated Date: 2025.12.25 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Any
@@ -30,6 +30,15 @@ class Media:
30
30
  else:
31
31
  self.window.ui.config['global']['img_raw'].setChecked(False)
32
32
 
33
+ # mode (image|video|music)
34
+ mode = self.window.core.config.get('img_mode', 'image')
35
+ self.window.controller.config.apply_value(
36
+ parent_id="global",
37
+ key="img_mode",
38
+ option=self.window.core.image.get_mode_option(),
39
+ value=mode,
40
+ )
41
+
33
42
  # image: resolution
34
43
  resolution = self.window.core.config.get('img_resolution', '1024x1024')
35
44
  self.window.controller.config.apply_value(
@@ -51,6 +60,7 @@ class Media:
51
60
  # -- add hooks --
52
61
  if not self.initialized:
53
62
  self.window.ui.add_hook("update.global.img_resolution", self.hook_update)
63
+ self.window.ui.add_hook("update.global.img_mode", self.hook_update)
54
64
  self.window.ui.add_hook("update.global.video.aspect_ratio", self.hook_update)
55
65
 
56
66
  def reload(self):
@@ -69,6 +79,11 @@ class Media:
69
79
  if not value:
70
80
  return
71
81
  self.window.core.config.set('img_resolution', value)
82
+ elif key == "img_mode":
83
+ if not value:
84
+ return
85
+ self.window.core.config.set('img_mode', value)
86
+ self.window.controller.ui.mode.update() # switch image|video options
72
87
  elif key == "video.aspect_ratio":
73
88
  if not value:
74
89
  return
@@ -92,6 +107,10 @@ class Media:
92
107
  else:
93
108
  self.enable_raw()
94
109
 
110
+ def get_mode(self) -> str:
111
+ """Get media generation mode (image/video/music)"""
112
+ return self.window.core.config.get("img_mode", "image")
113
+
95
114
  def is_image_model(self) -> bool:
96
115
  """
97
116
  Check if the model is an image generation model