pygpt-net 2.4.42__py3-none-any.whl → 2.4.45__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 (71) hide show
  1. CHANGELOG.md +15 -0
  2. README.md +21 -2
  3. pygpt_net/CHANGELOG.txt +15 -0
  4. pygpt_net/__init__.py +3 -3
  5. pygpt_net/controller/attachment.py +31 -3
  6. pygpt_net/controller/chat/attachment.py +37 -36
  7. pygpt_net/controller/config/placeholder.py +6 -4
  8. pygpt_net/controller/idx/common.py +7 -3
  9. pygpt_net/controller/lang/mapping.py +32 -9
  10. pygpt_net/core/attachments/__init__.py +7 -2
  11. pygpt_net/core/attachments/context.py +52 -34
  12. pygpt_net/core/db/__init__.py +2 -1
  13. pygpt_net/core/debug/attachments.py +1 -0
  14. pygpt_net/core/idx/__init__.py +8 -3
  15. pygpt_net/core/idx/indexing.py +24 -7
  16. pygpt_net/core/idx/ui/__init__.py +22 -0
  17. pygpt_net/core/idx/ui/loaders.py +217 -0
  18. pygpt_net/data/config/config.json +4 -4
  19. pygpt_net/data/config/models.json +3 -3
  20. pygpt_net/data/config/modes.json +3 -3
  21. pygpt_net/data/config/settings.json +5 -5
  22. pygpt_net/data/css/style.css +1 -0
  23. pygpt_net/data/locale/locale.de.ini +4 -4
  24. pygpt_net/data/locale/locale.en.ini +11 -9
  25. pygpt_net/data/locale/locale.es.ini +4 -4
  26. pygpt_net/data/locale/locale.fr.ini +4 -4
  27. pygpt_net/data/locale/locale.it.ini +4 -4
  28. pygpt_net/data/locale/locale.pl.ini +4 -4
  29. pygpt_net/data/locale/locale.uk.ini +4 -4
  30. pygpt_net/data/locale/locale.zh.ini +4 -4
  31. pygpt_net/data/locale/plugin.mailer.en.ini +5 -5
  32. pygpt_net/item/attachment.py +5 -1
  33. pygpt_net/item/ctx.py +99 -2
  34. pygpt_net/migrations/Version20241215110000.py +25 -0
  35. pygpt_net/migrations/__init__.py +3 -1
  36. pygpt_net/plugin/cmd_files/__init__.py +3 -2
  37. pygpt_net/provider/core/attachment/json_file.py +4 -1
  38. pygpt_net/provider/core/config/patch.py +12 -0
  39. pygpt_net/provider/core/ctx/db_sqlite/storage.py +50 -7
  40. pygpt_net/provider/core/ctx/db_sqlite/utils.py +29 -5
  41. pygpt_net/provider/loaders/base.py +14 -0
  42. pygpt_net/provider/loaders/hub/google/gmail.py +2 -2
  43. pygpt_net/provider/loaders/hub/yt/base.py +5 -0
  44. pygpt_net/provider/loaders/web_database.py +13 -5
  45. pygpt_net/provider/loaders/web_github_issues.py +18 -1
  46. pygpt_net/provider/loaders/web_github_repo.py +10 -0
  47. pygpt_net/provider/loaders/web_google_calendar.py +9 -1
  48. pygpt_net/provider/loaders/web_google_docs.py +6 -1
  49. pygpt_net/provider/loaders/web_google_drive.py +10 -1
  50. pygpt_net/provider/loaders/web_google_gmail.py +5 -3
  51. pygpt_net/provider/loaders/web_google_keep.py +5 -1
  52. pygpt_net/provider/loaders/web_google_sheets.py +5 -1
  53. pygpt_net/provider/loaders/web_microsoft_onedrive.py +15 -1
  54. pygpt_net/provider/loaders/web_page.py +4 -2
  55. pygpt_net/provider/loaders/web_rss.py +3 -1
  56. pygpt_net/provider/loaders/web_sitemap.py +9 -3
  57. pygpt_net/provider/loaders/web_twitter.py +4 -2
  58. pygpt_net/provider/loaders/web_yt.py +17 -2
  59. pygpt_net/provider/vector_stores/ctx_attachment.py +1 -1
  60. pygpt_net/tools/indexer/__init__.py +8 -40
  61. pygpt_net/tools/indexer/ui/web.py +33 -80
  62. pygpt_net/ui/layout/ctx/ctx_list.py +86 -18
  63. pygpt_net/ui/widget/dialog/url.py +162 -14
  64. pygpt_net/ui/widget/element/group.py +15 -2
  65. pygpt_net/ui/widget/lists/context.py +23 -9
  66. pygpt_net/utils.py +1 -1
  67. {pygpt_net-2.4.42.dist-info → pygpt_net-2.4.45.dist-info}/METADATA +22 -3
  68. {pygpt_net-2.4.42.dist-info → pygpt_net-2.4.45.dist-info}/RECORD +71 -68
  69. {pygpt_net-2.4.42.dist-info → pygpt_net-2.4.45.dist-info}/LICENSE +0 -0
  70. {pygpt_net-2.4.42.dist-info → pygpt_net-2.4.45.dist-info}/WHEEL +0 -0
  71. {pygpt_net-2.4.42.dist-info → pygpt_net-2.4.45.dist-info}/entry_points.txt +0 -0
@@ -6,12 +6,16 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2024.11.26 02:00:00 #
9
+ # Updated Date: 2024.12.16 01:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from PySide6.QtWidgets import QDialog, QLabel, QHBoxLayout, QVBoxLayout, QPushButton
12
+ from PySide6.QtCore import Qt
13
+ from PySide6.QtGui import QIcon
14
+ from PySide6.QtWidgets import QDialog, QLabel, QHBoxLayout, QVBoxLayout, QPushButton, QScrollArea, QWidget, QSizePolicy
13
15
 
14
- from pygpt_net.ui.widget.element.labels import HelpLabel
16
+ from pygpt_net.ui.widget.element.group import QHLine
17
+ from pygpt_net.ui.widget.element.labels import HelpLabel, UrlLabel, IconLabel
18
+ from pygpt_net.ui.widget.option.combo import OptionCombo
15
19
  from pygpt_net.utils import trans
16
20
  from pygpt_net.ui.widget.textarea.url import UrlInput
17
21
 
@@ -28,15 +32,19 @@ class UrlDialog(QDialog):
28
32
  self.window = window
29
33
  self.id = id
30
34
  self.current = None
31
- self.input = UrlInput(window, id)
32
- self.input.setMinimumWidth(400)
35
+ #self.input = UrlInput(window, id)
36
+ #self.input.setMinimumWidth(400)
37
+ self.initialized = False
38
+ self.params_scroll = None
39
+
40
+ def init(self):
41
+ """Initialize dialog"""
42
+ if self.initialized:
43
+ return
33
44
 
34
45
  self.window.ui.nodes['dialog.url.btn.update'] = QPushButton(trans('dialog.url.update'))
35
46
  self.window.ui.nodes['dialog.url.btn.update'].clicked.connect(
36
- lambda: self.window.controller.dialogs.confirm.accept_url(
37
- self.id,
38
- self.window.ui.dialog['url'].current,
39
- self.input.text()),
47
+ lambda: self.window.controller.attachment.attach_url()
40
48
  )
41
49
 
42
50
  self.window.ui.nodes['dialog.url.btn.dismiss'] = QPushButton(trans('dialog.url.dismiss'))
@@ -47,13 +55,153 @@ class UrlDialog(QDialog):
47
55
  bottom.addWidget(self.window.ui.nodes['dialog.url.btn.dismiss'])
48
56
  bottom.addWidget(self.window.ui.nodes['dialog.url.btn.update'])
49
57
 
50
- self.window.ui.nodes['dialog.url.label'] = QLabel(trans("dialog.url.title"))
51
- self.window.ui.nodes['dialog.url.tip'] = HelpLabel(trans("dialog.url.tip"))
58
+ #self.window.ui.nodes['dialog.url.label'] = QLabel(trans("dialog.url.title"))
59
+ #self.window.ui.nodes['dialog.url.tip'] = HelpLabel(trans("dialog.url.tip"))
60
+
61
+ # -----------------
62
+
63
+ loaders = self.window.controller.config.placeholder.apply_by_id("llama_index_loaders_web")
64
+ loaders_list = []
65
+ for loader in loaders:
66
+ k = list(loader.keys())[0]
67
+ key = k.replace("web_", "")
68
+ value = loader[k]
69
+ loaders_list.append({
70
+ key: value,
71
+ })
72
+
73
+ self.window.ui.nodes["dialog.url.loader"] = OptionCombo(
74
+ self.window,
75
+ "dialog.url",
76
+ "web.loader",
77
+ {
78
+ "label": trans("tool.indexer.tab.web.loader"),
79
+ "keys": loaders_list,
80
+ "value": "webpage",
81
+ }
82
+ )
83
+
84
+ self.window.ui.nodes["dialog.url.loader"].layout.setContentsMargins(0, 0, 0, 0)
85
+ self.window.ui.nodes["dialog.url.loader.label"] = HelpLabel(trans("tool.indexer.tab.web.loader"))
86
+ self.window.ui.add_hook("update.dialog.url.web.loader", self.hook_loader_change)
87
+
88
+ self.window.ui.nodes["dialog.url.options.label"] = HelpLabel(trans("tool.indexer.tab.web.source"))
89
+
90
+ config_label = HelpLabel(trans("tool.indexer.tab.web.cfg"))
91
+ config_label.setWordWrap(False)
92
+
93
+ config_label_layout = QHBoxLayout()
94
+ config_label_layout.addWidget(IconLabel(QIcon(":/icons/settings_filled.svg")))
95
+ config_label_layout.addWidget(config_label)
96
+ config_label_layout.setAlignment(Qt.AlignLeft)
97
+
98
+ self.window.ui.nodes["dialog.url.config.label"] = QWidget()
99
+ self.window.ui.nodes["dialog.url.config.label"].setLayout(config_label_layout)
100
+
101
+ self.window.ui.nodes["dialog.url.config.help"] = UrlLabel(
102
+ trans("tool.indexer.tab.web.help"),
103
+ "https://pygpt.readthedocs.io/en/latest/configuration.html#data-loaders")
104
+
105
+ params_layout = QVBoxLayout()
106
+ params_layout.setContentsMargins(0, 0, 0, 0)
107
+
108
+ self.params_scroll = QScrollArea()
109
+ self.params_scroll.setWidgetResizable(True)
110
+ self.window.ui.nodes["dialog.url.loader.option"] = {}
111
+ self.window.ui.nodes["dialog.url.loader.option_group"] = {}
112
+ self.window.ui.nodes["dialog.url.loader.config"] = {}
113
+ self.window.ui.nodes["dialog.url.loader.config_group"] = {}
114
+
115
+ # params
116
+ params_layout.addWidget(self.window.ui.nodes["dialog.url.options.label"])
117
+ inputs, groups = self.window.core.idx.ui.loaders.setup_loader_options()
118
+
119
+ for loader in inputs:
120
+ for k in inputs[loader]:
121
+ self.window.ui.nodes["dialog.url.loader.option." + loader + "." + k] = inputs[loader][k]
122
+ for loader in groups:
123
+ self.window.ui.nodes["dialog.url.loader.option_group"][loader] = groups[loader]
124
+ params_layout.addWidget(self.window.ui.nodes["dialog.url.loader.option_group"][loader])
125
+ self.window.ui.nodes["dialog.url.loader.option_group"][loader].hide() # hide on start
126
+
127
+ # separator
128
+ params_layout.addWidget(QHLine())
129
+
130
+ # config
131
+ params_layout.addWidget(self.window.ui.nodes["dialog.url.config.label"])
132
+ inputs, groups = self.window.core.idx.ui.loaders.setup_loader_config()
133
+
134
+ for loader in inputs:
135
+ for k in inputs[loader]:
136
+ self.window.ui.nodes["dialog.url.loader.config." + loader + "." + k] = inputs[loader][k]
137
+ for loader in groups:
138
+ self.window.ui.nodes["dialog.url.loader.config_group"][loader] = groups[loader]
139
+ params_layout.addWidget(self.window.ui.nodes["dialog.url.loader.config_group"][loader])
140
+ self.window.ui.nodes["dialog.url.loader.config_group"][loader].hide() # hide on start
141
+ params_layout.addWidget(self.window.ui.nodes["dialog.url.config.help"], alignment=Qt.AlignCenter)
142
+
143
+ # stretch
144
+ params_layout.addStretch(1)
145
+
146
+ self.params_widget = QWidget()
147
+ self.params_widget.setLayout(params_layout)
148
+ self.params_scroll.setWidget(self.params_widget)
149
+
150
+ # resize
151
+ self.params_scroll.setWidgetResizable(True)
152
+ self.params_scroll.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
153
+
154
+ # -----------------
52
155
 
53
156
  layout = QVBoxLayout()
54
- layout.addWidget(self.window.ui.nodes['dialog.url.label'])
55
- layout.addWidget(self.input)
56
- layout.addWidget(self.window.ui.nodes['dialog.url.tip'])
157
+ #layout.addWidget(self.window.ui.nodes['dialog.url.label'])
158
+ #layout.addWidget(self.input)
159
+ #layout.addWidget(self.window.ui.nodes['dialog.url.tip'])
160
+ layout.addWidget(self.window.ui.nodes['dialog.url.loader.label'])
161
+ layout.addWidget(self.window.ui.nodes['dialog.url.loader'])
162
+ layout.addWidget(self.params_scroll)
57
163
  layout.addLayout(bottom)
58
164
 
165
+ # defaults
166
+ self.window.ui.nodes["dialog.url.loader"].set_value("webpage")
167
+
59
168
  self.setLayout(layout)
169
+
170
+ self.initialized = True
171
+
172
+ def hook_loader_change(self, key, value, caller, *args, **kwargs):
173
+ """
174
+ Hook: on loader change
175
+
176
+ :param key: Option key
177
+ :param value: Option value
178
+ :param caller: Caller
179
+ :param args: Args
180
+ :param kwargs: Kwargs
181
+ """
182
+ # hide/show options
183
+ for loader in self.window.ui.nodes["dialog.url.loader.option_group"]:
184
+ self.window.ui.nodes["dialog.url.loader.option_group"][loader].hide()
185
+ if value in self.window.ui.nodes["dialog.url.loader.option_group"]:
186
+ self.window.ui.nodes["dialog.url.loader.option_group"][value].show()
187
+
188
+ # show/hide label if options are available
189
+ self.window.ui.nodes["dialog.url.options.label"].hide()
190
+ if value in self.window.ui.nodes["dialog.url.loader.option_group"]:
191
+ self.window.ui.nodes["dialog.url.options.label"].show()
192
+
193
+ # hide/show config
194
+ for loader in self.window.ui.nodes["dialog.url.loader.config_group"]:
195
+ self.window.ui.nodes["dialog.url.loader.config_group"][loader].hide()
196
+ if value in self.window.ui.nodes["dialog.url.loader.config_group"]:
197
+ self.window.ui.nodes["dialog.url.loader.config_group"][value].show()
198
+
199
+ # show/hide label if config are available
200
+ self.window.ui.nodes["dialog.url.config.label"].hide()
201
+ self.window.ui.nodes["dialog.url.config.help"].hide()
202
+ if value in self.window.ui.nodes["dialog.url.loader.config_group"]:
203
+ self.window.ui.nodes["dialog.url.config.label"].show()
204
+ self.window.ui.nodes["dialog.url.config.help"].show()
205
+
206
+ self.params_widget.adjustSize()
207
+ self.params_scroll.update()
@@ -6,11 +6,12 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2024.11.24 22:00:00 #
9
+ # Updated Date: 2024.12.16 01:00:00 #
10
10
  # ================================================== #
11
+
11
12
  from PySide6.QtCore import Qt
12
13
  from PySide6.QtGui import QIcon
13
- from PySide6.QtWidgets import QCheckBox, QWidget, QVBoxLayout
14
+ from PySide6.QtWidgets import QCheckBox, QWidget, QVBoxLayout, QFrame
14
15
 
15
16
  import pygpt_net.icons_rc
16
17
 
@@ -105,3 +106,15 @@ class CollapsedGroup(QWidget):
105
106
  :param option: option widget
106
107
  """
107
108
  self.options.addWidget(option)
109
+
110
+ class QHLine(QFrame):
111
+ def __init__(self):
112
+ super(QHLine, self).__init__()
113
+ self.setFrameShape(QFrame.HLine)
114
+ self.setFrameShadow(QFrame.Sunken)
115
+
116
+ class QVLine(QFrame):
117
+ def __init__(self):
118
+ super(QVLine, self).__init__()
119
+ self.setFrameShape(QFrame.VLine)
120
+ self.setFrameShadow(QFrame.Sunken)
@@ -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: 2024.12.12 04:00:00 #
9
+ # Updated Date: 2024.12.16 01:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import datetime
@@ -14,6 +14,7 @@ import datetime
14
14
  from PySide6 import QtWidgets, QtCore, QtGui
15
15
  from PySide6.QtGui import QAction, QIcon, QColor, QPixmap, QStandardItem
16
16
  from PySide6.QtWidgets import QMenu
17
+ from overrides import overrides
17
18
 
18
19
  from pygpt_net.ui.widget.lists.base import BaseList
19
20
  from pygpt_net.utils import trans
@@ -31,13 +32,13 @@ class ContextList(BaseList):
31
32
  super(ContextList, self).__init__(window)
32
33
  self.window = window
33
34
  self.id = id
34
- self.clicked.connect(self.click)
35
35
  self.expanded_items = set()
36
36
  self.setItemDelegate(ImportantItemDelegate())
37
37
 
38
+
38
39
  def click(self, index):
39
40
  """
40
- Click event
41
+ Click event (override, connected in BaseList class)
41
42
 
42
43
  :param index: index
43
44
  """
@@ -49,8 +50,10 @@ class ContextList(BaseList):
49
50
  self.window.controller.ctx.set_group(item.id)
50
51
  if self.window.ui.nodes['ctx.list'].isExpanded(index):
51
52
  self.expanded_items.discard(item.id)
53
+ self.window.ui.nodes['ctx.list'].collapse(index)
52
54
  else:
53
55
  self.expanded_items.add(item.id)
56
+ self.window.ui.nodes['ctx.list'].expand(index)
54
57
  else:
55
58
  self.window.controller.ctx.select_by_id(item.id)
56
59
  else:
@@ -76,7 +79,7 @@ class ContextList(BaseList):
76
79
 
77
80
  :param index: index
78
81
  """
79
- pass
82
+ print("dblclick")
80
83
 
81
84
  def mousePressEvent(self, event):
82
85
  """
@@ -407,20 +410,28 @@ class ImportantItemDelegate(QtWidgets.QStyledItemDelegate):
407
410
  label = 0
408
411
  is_important = False
409
412
  is_attachment = False
413
+ is_group = False
414
+ in_group = False
415
+
410
416
  if "label" in data:
411
417
  label = data["label"]
412
418
  if "is_important" in data and data["is_important"]:
413
419
  is_important = True
414
420
  if "is_attachment" in data and data["is_attachment"]:
415
421
  is_attachment = True
422
+ if "is_group" in data and data["is_group"]:
423
+ is_group = True
424
+ if "in_group" in data and data["in_group"]:
425
+ in_group = True
416
426
 
417
427
  painter.save()
418
428
 
419
429
  if is_attachment:
420
430
  icon = QtGui.QIcon(":/icons/attachment.svg")
421
431
  icon_size = option.decorationSize or QtCore.QSize(16, 16)
432
+ icon_pos = option.rect.right() - icon_size.width()
422
433
  icon_rect = QtCore.QRect(
423
- option.rect.right() - icon_size.width(),
434
+ icon_pos,
424
435
  option.rect.top() + (option.rect.height() - icon_size.height()) / 2,
425
436
  icon_size.width(),
426
437
  icon_size.height()
@@ -447,8 +458,6 @@ class ImportantItemDelegate(QtWidgets.QStyledItemDelegate):
447
458
  )
448
459
  painter.drawRect(square_rect)
449
460
 
450
- #label = label - 10 # remove pin status
451
-
452
461
  # label (0-9)
453
462
  if label > 0:
454
463
  color = self.get_color_for_status(label)
@@ -495,6 +504,7 @@ class GroupItem(QStandardItem):
495
504
  self.name = name
496
505
  self.isFolder = True
497
506
  self.isPinned = False
507
+ self.hasAttachments = False
498
508
  self.dt = None
499
509
 
500
510
  class Item(QStandardItem):
@@ -507,12 +517,16 @@ class Item(QStandardItem):
507
517
  self.dt = None
508
518
 
509
519
  class SectionItem(QStandardItem):
510
- def __init__(self, title):
520
+ def __init__(self, title, group: bool = False):
511
521
  super().__init__(title)
512
522
  self.title = title
523
+ self.group = group
513
524
  self.setSelectable(False)
514
525
  self.setEnabled(False)
515
- self.setTextAlignment(QtCore.Qt.AlignRight)
526
+ if self.group:
527
+ self.setTextAlignment(QtCore.Qt.AlignLeft)
528
+ else:
529
+ self.setTextAlignment(QtCore.Qt.AlignRight)
516
530
  font = self.font()
517
531
  font.setBold(True)
518
532
  self.setFont(font)
pygpt_net/utils.py CHANGED
@@ -139,7 +139,7 @@ def parse_args(data: list) -> dict:
139
139
  elif type == 'dict':
140
140
  try:
141
141
  args[key] = json.loads(value)
142
- except json.JSONDecodeError:
142
+ except:
143
143
  args[key] = {}
144
144
  elif type == 'list':
145
145
  args[key] = [x.strip() for x in value.split(',')]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pygpt-net
3
- Version: 2.4.42
3
+ Version: 2.4.45
4
4
  Summary: Desktop AI Assistant powered by models: OpenAI o1, GPT-4o, GPT-4, GPT-4 Vision, GPT-3.5, DALL-E 3, Llama 3, Mistral, Gemini, Claude, Bielik, and other models supported by Langchain, Llama Index, and Ollama. Features include chatbot, text completion, image generation, vision analysis, speech-to-text, internet access, file handling, command execution and more.
5
5
  Home-page: https://pygpt.net
6
6
  License: MIT
@@ -92,7 +92,7 @@ Description-Content-Type: text/markdown
92
92
 
93
93
  [![pygpt](https://snapcraft.io/pygpt/badge.svg)](https://snapcraft.io/pygpt)
94
94
 
95
- Release: **2.4.42** | build: **2024.12.15** | Python: **>=3.10, <3.12**
95
+ Release: **2.4.45** | build: **2024.12.16** | Python: **>=3.10, <3.12**
96
96
 
97
97
  > Official website: https://pygpt.net | Documentation: https://pygpt.readthedocs.io
98
98
  >
@@ -875,6 +875,8 @@ You can use your own files (for example, to analyze them) during any conversatio
875
875
 
876
876
  **PyGPT** makes it simple for users to upload files and send them to the model for tasks like analysis, similar to attaching files in `ChatGPT`. There's a separate `Attachments` tab next to the text input area specifically for managing file uploads.
877
877
 
878
+ **Tip: Attachments uploaded in group are available in all contexts in group**.
879
+
878
880
  ![v2_file_input](https://github.com/user-attachments/assets/db8467b6-2d07-4e20-a795-430fc09443a7)
879
881
 
880
882
  You can use attachments to provide additional context to the conversation. Uploaded files will be converted into text using loaders from LlamaIndex, and then embedded into the vector store. You can upload any file format supported by the application through LlamaIndex. Supported formats include:
@@ -3350,8 +3352,10 @@ Allowed additional keyword arguments for built-in data loaders (Web and external
3350
3352
 
3351
3353
  **SQL Database** (web_database)
3352
3354
 
3353
- - `engine` - str, default: `None`
3354
3355
  - `uri` - str, default: `None`
3356
+
3357
+ You can provide a single URI in the form of: `{scheme}://{user}:{password}@{host}:{port}/{dbname}`, or you can provide each field manually:
3358
+
3355
3359
  - `scheme` - str, default: `None`
3356
3360
  - `host` - str, default: `None`
3357
3361
  - `port` - str, default: `None`
@@ -4024,6 +4028,21 @@ may consume additional tokens that are not displayed in the main window.
4024
4028
 
4025
4029
  ## Recent changes:
4026
4030
 
4031
+ **2.4.45 (2024-12-16)**
4032
+
4033
+ - Enhanced web data loaders UI.
4034
+
4035
+ **2.4.44 (2024-12-16)**
4036
+
4037
+ - Enhanced web data loaders.
4038
+ - Web loaders have been added to attachments, allowing external web content to be attached to context via the "+Web" button in the Attachments tab.
4039
+ - Improved handling of attachments in groups and added an attachment icon when a group contains attachments.
4040
+
4041
+ **2.4.43 (2024-12-15)**
4042
+
4043
+ - Fix: Bug on attachment upload.
4044
+ - Added: Attachments uploaded in groups are now available for all contexts in the group (beta).
4045
+
4027
4046
  **2.4.42 (2024-12-15)**
4028
4047
 
4029
4048
  - Added Mailer plugin, which allows sending and retrieving emails from the server, and reading them. It currently supports only SMTP.