OllamaInterface/OllamaWidget.py

changeset 4
7dd1b9cd3150
child 5
6e8af43d537d
equal deleted inserted replaced
3:ca28466a186d 4:7dd1b9cd3150
1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
3 #
4
5 """
6 Module implementing the main ollama interface widget.
7 """
8
9 import json
10 import os
11
12 from PyQt6.QtCore import Qt, pyqtSlot
13 from PyQt6.QtWidgets import QInputDialog, QLineEdit, QVBoxLayout, QWidget
14
15 from eric7 import Globals
16 from eric7.EricGui import EricPixmapCache
17 from eric7.EricWidgets import EricMessageBox
18
19 from .OllamaClient import OllamaClient
20 from .OllamaHistoryWidget import OllamaHistoryWidget
21 from .Ui_OllamaWidget import Ui_OllamaWidget
22
23
24 class OllamaWidget(QWidget, Ui_OllamaWidget):
25 """
26 Class implementing the main ollama interface widget.
27 """
28
29 OllamaHistoryFile = "ollama_history.json"
30
31 def __init__(self, plugin, fromEric=True, parent=None):
32 """
33 Constructor
34
35 @param plugin reference to the plug-in object
36 @type PluginOllamaInterface
37 @param fromEric flag indicating the eric-ide mode (defaults to True)
38 (True = eric-ide mode, False = application mode)
39 @type bool (optional)
40 @param parent reference to the parent widget (defaults to None)
41 @type QWidget (optional)
42 """
43 super().__init__(parent)
44 self.setupUi(self)
45
46 self.__plugin = plugin
47 self.__client = OllamaClient(plugin, self)
48
49 if fromEric:
50 self.layout().setContentsMargins(0, 3, 0, 0)
51 else:
52 self.layout().setContentsMargins(0, 0, 0, 0)
53
54 self.ollamaMenuButton.setIcon(EricPixmapCache.getIcon("superMenu"))
55 self.reloadModelsButton.setIcon(EricPixmapCache.getIcon("reload"))
56 self.newChatButton.setIcon(EricPixmapCache.getIcon("plus"))
57
58 self.__chatHistoryLayout = QVBoxLayout()
59 self.historyScrollWidget.setLayout(self.__chatHistoryLayout)
60 self.__chatHistoryLayout.addStretch(1)
61
62 self.mainSplitter.setSizes([200, 2000])
63
64 self.newChatButton.setEnabled(False)
65 self.__handleServerStateChanged(False)
66
67 self.__connectClient()
68
69 self.__loadHistory()
70
71 def __connectClient(self):
72 """
73 Private method to connect the client signals.
74 """
75 self.__client.serverStateChanged.connect(self.__handleServerStateChanged)
76 self.__client.serverVersion.connect(self.__setHeaderLabel)
77 self.__client.modelsList.connect(self.__populateModelSelector)
78
79 @pyqtSlot(bool)
80 def __handleServerStateChanged(self, ok):
81 """
82 Private slot handling a change in the 'ollama' server responsiveness.
83
84 @param ok flag indicating a responsive 'ollama' server
85 @type bool
86 """
87 if ok:
88 self.__finishSetup()
89 else:
90 self.ollamaVersionLabel.setText(
91 self.tr("<b>Error: The configured server is not responding.</b>")
92 )
93 self.setEnabled(ok)
94
95 @pyqtSlot()
96 def __finishSetup(self):
97 """
98 Private slot to finish the UI setup.
99 """
100 self.__client.version()
101 self.__client.list()
102
103 @pyqtSlot()
104 def on_reloadModelsButton_clicked(self):
105 """
106 Private slot to reload the list of available models.
107 """
108 self.__client.list()
109
110 @pyqtSlot(str)
111 def on_modelComboBox_currentTextChanged(self, model):
112 """
113 Private slot handling the selection of a model.
114
115 @param model name of the selected model
116 @type str
117 """
118 self.newChatButton.setEnabled(bool(model))
119
120 @pyqtSlot()
121 def on_newChatButton_clicked(self):
122 """
123 Private slot to start a new chat with the 'ollama' server.
124 """
125 model = self.modelComboBox.currentText()
126 if not model:
127 EricMessageBox.critical(
128 self,
129 self.tr("New Chat"),
130 self.tr("""A model has to be selected first. Aborting..."""),
131 )
132 return
133
134 title, ok = QInputDialog.getText(
135 self,
136 self.tr("New Chat"),
137 self.tr("Enter a title for the new chat:"),
138 QLineEdit.EchoMode.Normal,
139 )
140 if ok and title:
141 self.__createHistoryWidget(title, model)
142 # TODO: create an empty chat widget for new chat
143
144 ############################################################################
145 ## Methods handling signals from the 'ollama' client.
146 ############################################################################
147
148 @pyqtSlot(str)
149 def __setHeaderLabel(self, version):
150 """
151 Private slot to receive the 'ollama' server version and set the header.
152
153 @param version 'ollama' server version'
154 @type str
155 """
156 self.ollamaVersionLabel.setText(
157 self.tr("<b>ollama Server Version {0}</b>").format(version)
158 )
159
160 @pyqtSlot(list)
161 def __populateModelSelector(self, modelNames):
162 """
163 Private slot to receive the list of available model names and populate
164 the model selector with them.
165
166 @param modelNames list of model names
167 @type list[str]
168 """
169 self.modelComboBox.clear()
170
171 self.modelComboBox.addItem("")
172 self.modelComboBox.addItems(sorted(modelNames))
173
174 ############################################################################
175 ## Methods handling signals from the chat history widgets.
176 ############################################################################
177
178 def __createHistoryWidget(self, title, model, jsonStr=None):
179 """
180 Private method to create a chat history widget and insert it into the
181 respective layout.
182
183 @param title title of the chat
184 @type str
185 @param model name of the model
186 @type str
187 @param jsonStr string containing JSON serialize chat history data (defaults
188 to None)
189 @type str (optional)
190 """
191 history = OllamaHistoryWidget(title=title, model=model, jsonStr=jsonStr)
192 self.__chatHistoryLayout.insertWidget(
193 self.__chatHistoryLayout.count() - 1, history
194 )
195
196 scrollbar = self.historyScrollArea.verticalScrollBar()
197 scrollbar.setMaximum(self.historyScrollWidget.height())
198 scrollbar.setValue(scrollbar.maximum())
199
200 history.deleteChatHistory.connect(self.__deleteHistory)
201 history.dataChanged.connect(self.__saveHistory)
202 history.newChatWithHistory.connect(self.__newChatWithHistory)
203
204 self.__saveHistory()
205
206 def __findHistoryWidgetIndex(self, uid):
207 """
208 Private method to find the index of the reference history widget.
209
210 @param uid ID of the history widget
211 @type str
212 @return index of the history widget
213 @rtype int
214 """
215 for index in range(self.__chatHistoryLayout.count() - 1):
216 widget = self.__chatHistoryLayout.itemAt(index).widget()
217 if widget.getId() == uid:
218 return index
219
220 return None
221
222 def __historyFilePath(self):
223 """
224 Private method to get the path name of the chat history file.
225
226 @return file path of the chat history file
227 @rtype str
228 """
229 return os.path.join(Globals.getConfigDir(), OllamaWidget.OllamaHistoryFile)
230
231 @pyqtSlot()
232 def __saveHistory(self):
233 """
234 Private method to save the current chat history to the history file.
235 """
236 # step 1: collect all history entries
237 entries = {}
238 for index in range(self.__chatHistoryLayout.count() - 1):
239 widget = self.__chatHistoryLayout.itemAt(index).widget()
240 uid = widget.getId()
241 entries[uid] = widget.saveToJson()
242
243 # step 2: save the collected chat histories
244 filePath = self.__historyFilePath()
245 try:
246 with open(filePath, "w") as f:
247 json.dump(entries, f)
248 except OSError as err:
249 EricMessageBox.critical(
250 self,
251 self.tr("Save Chat History"),
252 self.tr(
253 "<p>The chat history could not be saved to <b>{0}</b>.</p>"
254 "<p>Reason: {1}</p>"
255 ).format(filePath, str(err)),
256 )
257
258 def __loadHistory(self):
259 """
260 Private method to load a previously saved history file.
261 """
262 # step 1: load the history file, if it exists
263 filePath = self.__historyFilePath()
264 if not os.path.exists(filePath):
265 return
266
267 try:
268 with open(filePath, "r") as f:
269 entries = json.load(f)
270 except OSError as err:
271 EricMessageBox.critical(
272 self,
273 self.tr("Load Chat History"),
274 self.tr(
275 "<p>The chat history could not be loaded from <b>{0}</b>.</p>"
276 "<p>Reason: {1}</p>"
277 ).format(filePath, str(err)),
278 )
279 return
280
281 # step 2: create history widgets
282 for uid in entries:
283 self.__createHistoryWidget("", "", jsonStr=entries[uid])
284
285 def clearHistory(self):
286 """
287 Public method to clear the history entries and close all chats.
288 """
289 while self.__chatHistoryLayout.count() > 1:
290 # do not delete the spacer at the end of the list
291 item = self.__chatHistoryLayout.takeAt(0)
292 if item is not None:
293 item.widget().deleteLater()
294
295 self.__saveHistory()
296
297 @pyqtSlot(str)
298 def __deleteHistory(self, uid):
299 """
300 Private slot to delete the history with the given ID.
301
302 @param uid ID of the history to be deleted
303 @type str
304 """
305 widgetIndex = self.__findHistoryWidgetIndex(uid)
306 if widgetIndex is not None:
307 item = self.__chatHistoryLayout.takeAt(widgetIndex)
308 if item is not None:
309 item.widget().deleteLater()
310
311 self.__saveHistory()
312
313 @pyqtSlot(str)
314 def __newChatWithHistory(self, uid):
315 """
316 Private slot to start a new chat using a previously saved history.
317
318 @param uid ID of the history to be used
319 @type str
320 """
321 # TODO: not implemented yet
322 pass
323
324 #######################################################################
325 ## Menu related methods below
326 #######################################################################
327
328 def __initOllamaMenu(self):
329 """
330 Private method to create the super menu and attach it to the super
331 menu button.
332 """
333 # TODO: implement the menu and menu methods
334 # * Clear Chat History
335 # * Show Model Details
336 # * Show Model Processes
337 # * Pull Model
338 # * Show Model Shop (via a web browser)
339 # * Remove Model

eric ide

mercurial