OllamaInterface/OllamaWidget.py

changeset 6
d8064fb63eac
parent 5
6e8af43d537d
child 7
eb1dec15b2f0
equal deleted inserted replaced
5:6e8af43d537d 6:d8064fb63eac
8 8
9 import json 9 import json
10 import os 10 import os
11 11
12 from PyQt6.QtCore import Qt, QTimer, pyqtSlot 12 from PyQt6.QtCore import Qt, QTimer, pyqtSlot
13 from PyQt6.QtWidgets import QInputDialog, QLineEdit, QVBoxLayout, QWidget 13 from PyQt6.QtWidgets import (
14 QDialog,
15 QInputDialog,
16 QLineEdit,
17 QMenu,
18 QVBoxLayout,
19 QWidget,
20 )
14 21
15 from eric7 import Globals 22 from eric7 import Globals
16 from eric7.EricGui import EricPixmapCache 23 from eric7.EricGui import EricPixmapCache
17 from eric7.EricWidgets import EricMessageBox 24 from eric7.EricWidgets import EricFileDialog, EricMessageBox
18 from eric7.EricWidgets.EricApplication import ericApp 25 from eric7.EricWidgets.EricApplication import ericApp
26 from eric7.EricWidgets.EricListSelectionDialog import EricListSelectionDialog
19 27
20 from .OllamaChatWidget import OllamaChatWidget 28 from .OllamaChatWidget import OllamaChatWidget
21 from .OllamaClient import OllamaClient 29 from .OllamaClient import OllamaClient
22 from .OllamaHistoryWidget import OllamaHistoryWidget 30 from .OllamaHistoryWidget import OllamaHistoryWidget
23 from .Ui_OllamaWidget import Ui_OllamaWidget 31 from .Ui_OllamaWidget import Ui_OllamaWidget
62 EricPixmapCache.getIcon( 70 EricPixmapCache.getIcon(
63 os.path.join("OllamaInterface", "icons", "send{0}".format(iconSuffix)) 71 os.path.join("OllamaInterface", "icons", "send{0}".format(iconSuffix))
64 ) 72 )
65 ) 73 )
66 74
75 self.ollamaMenuButton.setAutoRaise(True)
76 self.ollamaMenuButton.setShowMenuInside(True)
77
67 self.__chatHistoryLayout = QVBoxLayout() 78 self.__chatHistoryLayout = QVBoxLayout()
68 self.historyScrollWidget.setLayout(self.__chatHistoryLayout) 79 self.historyScrollWidget.setLayout(self.__chatHistoryLayout)
69 self.__chatHistoryLayout.addStretch(1) 80 self.__chatHistoryLayout.addStretch(1)
70 81
71 self.mainSplitter.setSizes([200, 2000]) 82 self.mainSplitter.setSizes([200, 2000])
73 self.newChatButton.setEnabled(False) 84 self.newChatButton.setEnabled(False)
74 self.__handleServerStateChanged(False) 85 self.__handleServerStateChanged(False)
75 86
76 self.__connectClient() 87 self.__connectClient()
77 88
89 self.__initOllamaMenu()
90
78 self.sendButton.clicked.connect(self.__sendMessage) 91 self.sendButton.clicked.connect(self.__sendMessage)
79 self.messageEdit.returnPressed.connect(self.__sendMessage) 92 self.messageEdit.returnPressed.connect(self.__sendMessage)
80 93
81 self.__loadHistory() 94 self.__loadHistory()
82 self.__updateMessageEditState() 95 self.__updateMessageEditState()
86 Private method to connect the client signals. 99 Private method to connect the client signals.
87 """ 100 """
88 self.__client.serverStateChanged.connect(self.__handleServerStateChanged) 101 self.__client.serverStateChanged.connect(self.__handleServerStateChanged)
89 self.__client.serverVersion.connect(self.__setHeaderLabel) 102 self.__client.serverVersion.connect(self.__setHeaderLabel)
90 self.__client.modelsList.connect(self.__populateModelSelector) 103 self.__client.modelsList.connect(self.__populateModelSelector)
104 self.__client.modelsList.connect(self.__checkHistoryModels)
91 self.__client.replyReceived.connect(self.__handleServerMessage) 105 self.__client.replyReceived.connect(self.__handleServerMessage)
92 106
93 @pyqtSlot(bool) 107 @pyqtSlot(bool)
94 def __handleServerStateChanged(self, ok): 108 def __handleServerStateChanged(self, ok):
95 """ 109 """
159 self.modelComboBox.clear() 173 self.modelComboBox.clear()
160 174
161 self.modelComboBox.addItem("") 175 self.modelComboBox.addItem("")
162 self.modelComboBox.addItems(sorted(modelNames)) 176 self.modelComboBox.addItems(sorted(modelNames))
163 177
178 @pyqtSlot(list)
179 def __checkHistoryModels(self, modelNames):
180 """
181 Private slot to set the chat history entry states according to available
182 models.
183
184 @param modelNames list of model names
185 @type list[str]
186 """
187 for index in range(self.__chatHistoryLayout.count() - 1):
188 self.__chatHistoryLayout.itemAt(index).widget().checkModelAvailable(
189 modelNames
190 )
191
164 ############################################################################ 192 ############################################################################
165 ## Methods handling signals from the chat history widgets. 193 ## Methods handling signals from the chat history widgets.
166 ############################################################################ 194 ############################################################################
167 195
168 def __createHistoryWidget(self, title, model, jsonStr=None): 196 def __createHistoryWidget(self, title, model, jsonStr=None):
217 widget = self.__chatHistoryLayout.itemAt(index).widget() 245 widget = self.__chatHistoryLayout.itemAt(index).widget()
218 if widget.getId() == hid: 246 if widget.getId() == hid:
219 return widget 247 return widget
220 248
221 return None 249 return None
250
251 def __getHistoryIds(self):
252 """
253 Private method to get a list of all history IDs.
254
255 @return list of history IDs
256 @rtype list[str]
257 """
258 hids = []
259 for index in range(self.__chatHistoryLayout.count() - 1):
260 widget = self.__chatHistoryLayout.itemAt(index).widget()
261 hids.append(widget.getId())
262
263 return hids
222 264
223 def __historyFilePath(self): 265 def __historyFilePath(self):
224 """ 266 """
225 Private method to get the path name of the chat history file. 267 Private method to get the path name of the chat history file.
226 268
241 hid = widget.getId() 283 hid = widget.getId()
242 entries[hid] = widget.saveToJson() 284 entries[hid] = widget.saveToJson()
243 285
244 # step 2: save the collected chat histories 286 # step 2: save the collected chat histories
245 filePath = self.__historyFilePath() 287 filePath = self.__historyFilePath()
288 self.__saveChatHistoryFile(filePath, entries)
289
290 def __saveChatHistoryFile(self, filePath, entries):
291 """
292 Private method to save the chat history entries to a file.
293
294 @param filePath file name to save to
295 @type str
296 @param entries dictionary containing the chat history entries as a
297 JSON serialized string indexed by their ID
298 @type dict[str, str]
299 """
246 try: 300 try:
247 with open(filePath, "w") as f: 301 with open(filePath, "w") as f:
248 json.dump(entries, f) 302 json.dump(entries, f)
249 except OSError as err: 303 except OSError as err:
250 EricMessageBox.critical( 304 EricMessageBox.critical(
260 """ 314 """
261 Private method to load a previously saved history file. 315 Private method to load a previously saved history file.
262 """ 316 """
263 # step 1: load the history file, if it exists 317 # step 1: load the history file, if it exists
264 filePath = self.__historyFilePath() 318 filePath = self.__historyFilePath()
319 self.__loadChatHistoriesFile(filePath)
320
321 def __loadChatHistoriesFile(self, filePath, reportDuplicates=False):
322 """
323 Private method to load chat history entries from a given file.
324
325 @param filePath path of the chat history file
326 @type str
327 @param reportDuplicates flag indicating to report skipped chat history entries
328 (defaults to False)
329 @type bool (optional)
330 @return flag indicating success
331 @rtype str
332 """
265 if not os.path.exists(filePath): 333 if not os.path.exists(filePath):
266 return 334 return False
267 335
268 try: 336 try:
269 with open(filePath, "r") as f: 337 with open(filePath, "r") as f:
270 entries = json.load(f) 338 entries = json.load(f)
271 except OSError as err: 339 except OSError as err:
275 self.tr( 343 self.tr(
276 "<p>The chat history could not be loaded from <b>{0}</b>.</p>" 344 "<p>The chat history could not be loaded from <b>{0}</b>.</p>"
277 "<p>Reason: {1}</p>" 345 "<p>Reason: {1}</p>"
278 ).format(filePath, str(err)), 346 ).format(filePath, str(err)),
279 ) 347 )
280 return 348 return False
281 349
282 # step 2: create history widgets 350 # step 2: create history widgets
351 existingIDs = self.__getHistoryIds()
352 skipped = []
283 for hid in entries: 353 for hid in entries:
284 self.__createHistoryWidget("", "", jsonStr=entries[hid]) 354 if hid in existingIDs:
355 data = json.loads(entries[hid])
356 skipped.append(data["title"])
357 else:
358 self.__createHistoryWidget("", "", jsonStr=entries[hid])
359
360 if skipped and reportDuplicates:
361 EricMessageBox.warning(
362 self,
363 self.tr("Load Chat History"),
364 self.tr(
365 "<p>These chats were not loaded because they already existed.</p>"
366 "{0}"
367 ).format("<ul><li>{0}</li></ul>".format("</li><li>".join(skipped))),
368 )
369
370 return True
285 371
286 def clearHistory(self): 372 def clearHistory(self):
287 """ 373 """
288 Public method to clear the history entries and close all chats. 374 Public method to clear the history entries and close all chats.
289 """ 375 """
290 while self.__chatHistoryLayout.count() > 1: 376 while self.__chatHistoryLayout.count() > 1:
291 # do not delete the spacer at the end of the list 377 # do not delete the spacer at the end of the list
292 item = self.__chatHistoryLayout.takeAt(0) 378 item = self.__chatHistoryLayout.takeAt(0)
293 if item is not None: 379 if item is not None:
380 hid = item.widget().getId()
381 self.__removeChatWidget(hid)
294 item.widget().deleteLater() 382 item.widget().deleteLater()
295 383
296 self.__saveHistory() 384 self.__saveHistory()
297 385
298 @pyqtSlot(str) 386 @pyqtSlot(str)
506 """ 594 """
507 Private method to create the super menu and attach it to the super 595 Private method to create the super menu and attach it to the super
508 menu button. 596 menu button.
509 """ 597 """
510 # TODO: implement the menu and menu methods 598 # TODO: implement the menu and menu methods
511 # * Clear Chat History
512 # * Show Model Details 599 # * Show Model Details
513 # * Show Model Processes 600 # * Show Model Processes
514 # * Pull Model 601 # * Pull Model
515 # * Show Model Shop (via a web browser) 602 # * Show Model Shop (via a web browser)
516 # * Remove Model 603 # * Remove Model
604 # * Local Server
605 # * Start
606 # * Stop
607 ###################################################################
608 ## Menu with Chat History related actions
609 ###################################################################
610
611 self.__chatHistoryMenu = QMenu(self.tr("Chat History"))
612 self.__chatHistoryMenu.addAction(self.tr("Load"), self.__loadHistory)
613 self.__chatHistoryMenu.addSeparator()
614 self.__clearHistoriesAct = self.__chatHistoryMenu.addAction(
615 self.tr("Clear All"), self.__menuClearAllHistories
616 )
617 self.__chatHistoryMenu.addSeparator()
618 self.__chatHistoryMenu.addAction(self.tr("Import"), self.__menuImportHistories)
619 self.__chatHistoryMenu.addAction(self.tr("Export"), self.__menuExportHistories)
620
621 ###################################################################
622 ## Main menu
623 ###################################################################
624
625 self.__ollamaMenu = QMenu()
626 self.__ollamaMenu.addMenu(self.__chatHistoryMenu)
627 self.__ollamaMenu.addSeparator()
628 self.__ollamaMenu.addAction(self.tr("Configure..."), self.__ollamaConfigure)
629
630 self.__ollamaMenu.aboutToShow.connect(self.__aboutToShowOllamaMenu)
631
632 self.ollamaMenuButton.setMenu(self.__ollamaMenu)
633
634 @pyqtSlot()
635 def __aboutToShowOllamaMenu(self):
636 """
637 Private slot to set the action enabled status.
638 """
639 self.__clearHistoriesAct.setEnabled(self.__chatHistoryLayout.count() > 1)
640
641 @pyqtSlot()
642 def __ollamaConfigure(self):
643 """
644 Private slot to show the ollama configuration page.
645 """
646 ericApp().getObject("UserInterface").showPreferences("ollamaPage")
647
648 @pyqtSlot()
649 def __menuClearAllHistories(self):
650 """
651 Private slot to clear all chat history entries.
652 """
653 yes = EricMessageBox.yesNo(
654 self,
655 self.tr("Clear All Chat Histories"),
656 self.tr(
657 "<p>Do you really want to delete all chat histories? This is"
658 " <b>irreversible</b>.</p>"
659 ),
660 )
661 if yes:
662 self.clearHistory()
663
664 @pyqtSlot()
665 def __menuImportHistories(self):
666 """
667 Private slot to import chat history entries from a file.
668 """
669 historyFile = EricFileDialog.getOpenFileName(
670 self,
671 self.tr("Import Chat History"),
672 "",
673 self.tr("Chat History Files (*.json);;All Files (*)"),
674 self.tr("Chat History Files (*.json)"),
675 )
676 if historyFile:
677 self.__loadChatHistoriesFile(historyFile, reportDuplicates=True)
678
679 @pyqtSlot()
680 def __menuExportHistories(self):
681 """
682 Private slot to export chat history entries to a file.
683 """
684 entries = []
685 for index in range(self.__chatHistoryLayout.count() - 1):
686 item = self.__chatHistoryLayout.itemAt(index)
687 widget = item.widget()
688 hid = widget.getId()
689 title = widget.getTitle()
690 entries.append((title, hid))
691
692 dlg = EricListSelectionDialog(
693 entries,
694 title=self.tr("Export Chat History"),
695 message=self.tr("Select the chats to be exported:"),
696 checkBoxSelection=True,
697 showSelectAll=True,
698 )
699 if dlg.exec() == QDialog.DialogCode.Accepted:
700 selectedChats = dlg.getSelection()
701
702 fileName = EricFileDialog.getSaveFileName(
703 self,
704 self.tr("Export Chat History"),
705 "",
706 self.tr("Chat History Files (*.json)"),
707 None,
708 EricFileDialog.DontConfirmOverwrite,
709 )
710 if fileName:
711 if not fileName.endswith(".json"):
712 fileName += ".json"
713
714 entries = {}
715 for _, hid in selectedChats:
716 historyWidget = self.__findHistoryWidget(hid)
717 if historyWidget is not None:
718 entries[hid] = historyWidget.saveToJson()
719 self.__saveChatHistoryFile(fileName, entries)

eric ide

mercurial