Thu, 08 Aug 2024 18:33:49 +0200
Implemented a menu, dialog and actions to start a local 'ollama' server with the capability to monitor its log output.
--- a/OllamaInterface/OllamaClient.py Wed Aug 07 18:19:25 2024 +0200 +++ b/OllamaInterface/OllamaClient.py Thu Aug 08 18:33:49 2024 +0200 @@ -95,12 +95,23 @@ self.__heartbeatTimer.timeout.connect(self.__periodicHeartbeat) self.__state = OllamaClientState.Waiting + self.__localServer = False self.__serverResponding = False # start with a faulty state self.__plugin.preferencesChanged.connect(self.__setHeartbeatTimer) self.__setHeartbeatTimer() + def setMode(self, local): + """ + Public method to set the client mode to local. + + @param local flag indicating to connect to a locally started ollama server + @type bool + """ + self.__localServer = local + self.__serverResponding = False + def chat(self, model, messages, streaming=True): """ Public method to request a chat completion from the 'ollama' server. @@ -353,8 +364,16 @@ ollamaUrl = QUrl( "{0}://{1}:{2}/api/{3}".format( self.__plugin.getPreferences("OllamaScheme"), - self.__plugin.getPreferences("OllamaHost"), - self.__plugin.getPreferences("OllamaPort"), + ( + "127.0.0.1" + if self.__localServer + else self.__plugin.getPreferences("OllamaHost") + ), + ( + self.__plugin.getPreferences("OllamaLocalPort") + if self.__localServer + else self.__plugin.getPreferences("OllamaPort") + ), endpoint, ) )
--- a/OllamaInterface/OllamaWidget.py Wed Aug 07 18:19:25 2024 +0200 +++ b/OllamaInterface/OllamaWidget.py Thu Aug 08 18:33:49 2024 +0200 @@ -9,7 +9,7 @@ import json import os -from PyQt6.QtCore import Qt, QTimer, pyqtSlot +from PyQt6.QtCore import QProcessEnvironment, Qt, QTimer, pyqtSlot from PyQt6.QtWidgets import ( QDialog, QInputDialog, @@ -84,6 +84,9 @@ self.newChatButton.setEnabled(False) self.__handleServerStateChanged(False) + self.__localServerDialog = None + self.__localServerProcess = None + self.__connectClient() self.__initOllamaMenu() @@ -619,12 +622,30 @@ self.__chatHistoryMenu.addAction(self.tr("Export"), self.__menuExportHistories) ################################################################### + ## Menu with Local Server related actions + ################################################################### + + self.__localServerMenu = QMenu(self.tr("Local Server")) + self.__localServerStartMonitorAct = self.__localServerMenu.addAction( + self.tr("Start with Monitoring"), self.__startLocalServerMonitoring + ) + self.__localServerMenu.addSeparator() + self.__startLocalServerAct = self.__localServerMenu.addAction( + self.tr("Start"), self.__startLocalServer + ) + self.__stopLocalServerAct = self.__localServerMenu.addAction( + self.tr("Stop"), self.__stopLocalServer + ) + + ################################################################### ## Main menu ################################################################### self.__ollamaMenu = QMenu() self.__ollamaMenu.addMenu(self.__chatHistoryMenu) self.__ollamaMenu.addSeparator() + self.__ollamaMenu.addMenu(self.__localServerMenu) + self.__ollamaMenu.addSeparator() self.__ollamaMenu.addAction(self.tr("Configure..."), self.__ollamaConfigure) self.__ollamaMenu.aboutToShow.connect(self.__aboutToShowOllamaMenu) @@ -638,6 +659,16 @@ """ self.__clearHistoriesAct.setEnabled(self.__chatHistoryLayout.count() > 1) + self.__localServerStartMonitorAct.setEnabled( + self.__localServerProcess is None and self.__localServerDialog is None + ) + self.__startLocalServerAct.setEnabled( + self.__localServerProcess is None and self.__localServerDialog is None + ) + self.__stopLocalServerAct.setEnabled( + self.__localServerProcess is not None and self.__localServerDialog is None + ) + @pyqtSlot() def __ollamaConfigure(self): """ @@ -717,3 +748,76 @@ if historyWidget is not None: entries[hid] = historyWidget.saveToJson() self.__saveChatHistoryFile(fileName, entries) + + def prepareServerRuntimeEnvironment(self): + """ + Public method to prepare a QProcessEnvironment object. + + @return prepared environment object to be used with QProcess + @rtype QProcessEnvironment + """ + env = QProcessEnvironment.systemEnvironment() + env.insert( + "OLLAMA_HOST", + "127.0.0.1:{0}".format(self.__plugin.getPreferences("OllamaLocalPort")), + ) + + return env + + @pyqtSlot() + def __startLocalServerMonitoring(self): + """ + Private slot to open a dialog for running a local 'ollama' server instance + and monitor its output. + """ + # TODO: not implemented yet + from .RunOllamaServerDialog import RunOllamaServerDialog + + self.__localServerDialog = RunOllamaServerDialog( + self.__client, self.__plugin, self + ) + self.__localServerDialog.serverStarted.connect(self.__serverStarted) + self.__localServerDialog.serverStopped.connect(self.__serverStopped) + self.__localServerDialog.finished.connect(self.__serverDialogClosed) + self.__localServerDialog.show() + self.__localServerDialog.startServer() + + @pyqtSlot() + def __startLocalServer(self): + """ + Private slot to start a local 'ollama' server instance in the background. + """ + # TODO: not implemented yet + pass + + @pyqtSlot() + def __stopLocalServer(self): + """ + Private slot to stop a running local 'ollama' server instance. + """ + # TODO: not implemented yet + pass + + @pyqtSlot() + def __serverStarted(self): + """ + Private slot to handle the start of a local server. + """ + self.__client.setMode(True) + self.on_reloadModelsButton_clicked() + + @pyqtSlot() + def __serverStopped(self): + """ + Private slot to handle the stopping of a local server. + """ + self.__client.setMode(False) + self.on_reloadModelsButton_clicked() + + @pyqtSlot() + def __serverDialogClosed(self): + """ + Private slot handling the closing of the local server dialog. + """ + self.__localServerDialog.deleteLater() + self.__localServerDialog = None
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OllamaInterface/RunOllamaServerDialog.py Thu Aug 08 18:33:49 2024 +0200 @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 - 2024 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to run the ollama server locally. +""" + +from PyQt6.QtCore import QProcess, Qt, QTimer, pyqtSignal, pyqtSlot +from PyQt6.QtWidgets import QDialog, QDialogButtonBox + +from eric7.EricWidgets import EricMessageBox + +from .Ui_RunOllamaServerDialog import Ui_RunOllamaServerDialog + + +class RunOllamaServerDialog(QDialog, Ui_RunOllamaServerDialog): + """ + Class implementing a dialog to run the ollama server locally. + + @signal serverStarted() emitted after the start of the 'ollama' server + @signal serverStopped() emitted after the 'ollama' server was stopped + """ + + serverStarted = pyqtSignal() + serverStopped = pyqtSignal() + + def __init__(self, ollamaClient, plugin, parent=None): + """ + Constructor + + @param ollamaClient reference to the 'ollama' client object + @type OllamaClient + @param plugin reference to the plug-in object + @type PluginOllamaInterface + @param parent reference to the parent widget + @type QWidget + """ + super().__init__(parent) + self.setupUi(self) + + self.__plugin = plugin + self.__ui = parent + self.__client = ollamaClient + + self.__process = None + + self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(True) + self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setDefault(True) + + self.__defaultTextFormat = self.outputEdit.currentCharFormat() + + def startServer(self): + """ + Public method to start the ollama server process. + + @return flag indicating success + @rtype bool + """ + env = self.__ui.prepareServerRuntimeEnvironment() + self.__process = QProcess() + self.__process.setProcessEnvironment(env) + self.__process.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels) + + self.__process.readyReadStandardOutput.connect(self.__readStdOut) + self.__process.finished.connect(self.__processFinished) + + self.outputEdit.clear() + + command = "ollama" + args = ["serve"] + + self.__process.start(command, args) + ok = self.__process.waitForStarted(10000) + if not ok: + EricMessageBox.critical( + None, + self.tr("Run Local ollama Server"), + self.tr("""The loacl ollama server process could not be started."""), + ) + else: + self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled( + False + ) + self.stopServerButton.setEnabled(True) + self.stopServerButton.setDefault(True) + self.restartServerButton.setEnabled(True) + + self.serverStarted.emit() + + return ok + + def closeEvent(self, evt): + """ + Protected method handling a close event. + + @param evt reference to the close event + @type QCloseEvent + """ + self.on_stopServerButton_clicked() + evt.accept() + + @pyqtSlot() + def __readStdOut(self): + """ + Private slot to add the server process output to the output pane. + """ + if self.__process is not None: + out = str(self.__process.readAllStandardOutput(), "utf-8") + self.outputEdit.insertPlainText(out) + + @pyqtSlot() + def __processFinished(self): + """ + Private slot handling the finishing of the server process. + """ + if ( + self.__process is not None + and self.__process.state() != QProcess.ProcessState.NotRunning + ): + self.__process.terminate() + QTimer.singleShot(2000, self.__process.kill) + self.__process.waitForFinished(3000) + + self.__process = None + + self.restartServerButton.setEnabled(True) + self.stopServerButton.setEnabled(False) + self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(True) + self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setDefault(True) + self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setFocus( + Qt.FocusReason.OtherFocusReason + ) + + self.serverStopped.emit() + + @pyqtSlot() + def on_stopServerButton_clicked(self): + """ + Private slot to stop the running server. + """ + self.__process.terminate() + + @pyqtSlot() + def on_restartServerButton_clicked(self): + """ + Private slot to re-start the ollama server. + """ + # step 1: stop the current server + if self.__process is not None: + self.on_stopServerButton_clicked() + + # step 2: start a new server + self.startServer()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OllamaInterface/RunOllamaServerDialog.ui Thu Aug 08 18:33:49 2024 +0200 @@ -0,0 +1,124 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>RunOllamaServerDialog</class> + <widget class="QDialog" name="RunOllamaServerDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>600</width> + <height>500</height> + </rect> + </property> + <property name="windowTitle"> + <string>ollama Server</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Output</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QPlainTextEdit" name="outputEdit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="lineWrapMode"> + <enum>QPlainTextEdit::WidgetWidth</enum> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QPushButton" name="restartServerButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Press to restart the loacl ollama server.</string> + </property> + <property name="text"> + <string>Re-Start Server</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="stopServerButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Press to stop the running ollama server.</string> + </property> + <property name="text"> + <string>Stop Server</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <tabstops> + <tabstop>outputEdit</tabstop> + <tabstop>restartServerButton</tabstop> + <tabstop>stopServerButton</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>RunOllamaServerDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>505</x> + <y>474</y> + </hint> + <hint type="destinationlabel"> + <x>593</x> + <y>419</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OllamaInterface/Ui_RunOllamaServerDialog.py Thu Aug 08 18:33:49 2024 +0200 @@ -0,0 +1,66 @@ +# Form implementation generated from reading ui file 'OllamaInterface/RunOllamaServerDialog.ui' +# +# Created by: PyQt6 UI code generator 6.7.1 +# +# WARNING: Any manual changes made to this file will be lost when pyuic6 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt6 import QtCore, QtGui, QtWidgets + + +class Ui_RunOllamaServerDialog(object): + def setupUi(self, RunOllamaServerDialog): + RunOllamaServerDialog.setObjectName("RunOllamaServerDialog") + RunOllamaServerDialog.resize(600, 500) + RunOllamaServerDialog.setSizeGripEnabled(True) + self.verticalLayout_2 = QtWidgets.QVBoxLayout(RunOllamaServerDialog) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.groupBox = QtWidgets.QGroupBox(parent=RunOllamaServerDialog) + self.groupBox.setObjectName("groupBox") + self.verticalLayout = QtWidgets.QVBoxLayout(self.groupBox) + self.verticalLayout.setObjectName("verticalLayout") + self.outputEdit = QtWidgets.QPlainTextEdit(parent=self.groupBox) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.outputEdit.sizePolicy().hasHeightForWidth()) + self.outputEdit.setSizePolicy(sizePolicy) + self.outputEdit.setLineWrapMode(QtWidgets.QPlainTextEdit.LineWrapMode.WidgetWidth) + self.outputEdit.setReadOnly(True) + self.outputEdit.setObjectName("outputEdit") + self.verticalLayout.addWidget(self.outputEdit) + self.verticalLayout_2.addWidget(self.groupBox) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.restartServerButton = QtWidgets.QPushButton(parent=RunOllamaServerDialog) + self.restartServerButton.setEnabled(False) + self.restartServerButton.setObjectName("restartServerButton") + self.horizontalLayout.addWidget(self.restartServerButton) + self.stopServerButton = QtWidgets.QPushButton(parent=RunOllamaServerDialog) + self.stopServerButton.setEnabled(False) + self.stopServerButton.setObjectName("stopServerButton") + self.horizontalLayout.addWidget(self.stopServerButton) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout.addItem(spacerItem) + self.buttonBox = QtWidgets.QDialogButtonBox(parent=RunOllamaServerDialog) + self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Close) + self.buttonBox.setObjectName("buttonBox") + self.horizontalLayout.addWidget(self.buttonBox) + self.verticalLayout_2.addLayout(self.horizontalLayout) + + self.retranslateUi(RunOllamaServerDialog) + self.buttonBox.rejected.connect(RunOllamaServerDialog.reject) # type: ignore + QtCore.QMetaObject.connectSlotsByName(RunOllamaServerDialog) + RunOllamaServerDialog.setTabOrder(self.outputEdit, self.restartServerButton) + RunOllamaServerDialog.setTabOrder(self.restartServerButton, self.stopServerButton) + + def retranslateUi(self, RunOllamaServerDialog): + _translate = QtCore.QCoreApplication.translate + RunOllamaServerDialog.setWindowTitle(_translate("RunOllamaServerDialog", "ollama Server")) + self.groupBox.setTitle(_translate("RunOllamaServerDialog", "Output")) + self.restartServerButton.setToolTip(_translate("RunOllamaServerDialog", "Press to restart the loacl ollama server.")) + self.restartServerButton.setText(_translate("RunOllamaServerDialog", "Re-Start Server")) + self.stopServerButton.setToolTip(_translate("RunOllamaServerDialog", "Press to stop the running ollama server.")) + self.stopServerButton.setText(_translate("RunOllamaServerDialog", "Stop Server"))
--- a/PluginAiOllama.epj Wed Aug 07 18:19:25 2024 +0200 +++ b/PluginAiOllama.epj Thu Aug 08 18:33:49 2024 +0200 @@ -200,7 +200,8 @@ "FORMS": [ "OllamaInterface/OllamaChatWidget.ui", "OllamaInterface/OllamaHistoryWidget.ui", - "OllamaInterface/OllamaWidget.ui" + "OllamaInterface/OllamaWidget.ui", + "OllamaInterface/RunOllamaServerDialog.ui" ], "HASH": "92d9e369bad01266911c1d6eefedae578e76ceb4", "IDLPARAMS": { @@ -297,9 +298,11 @@ "OllamaInterface/OllamaClient.py", "OllamaInterface/OllamaHistoryWidget.py", "OllamaInterface/OllamaWidget.py", + "OllamaInterface/RunOllamaServerDialog.py", "OllamaInterface/Ui_OllamaChatWidget.py", "OllamaInterface/Ui_OllamaHistoryWidget.py", "OllamaInterface/Ui_OllamaWidget.py", + "OllamaInterface/Ui_RunOllamaServerDialog.py", "OllamaInterface/__init__.py", "PluginAiOllama.py", "__init__.py"
--- a/PluginAiOllama.py Wed Aug 07 18:19:25 2024 +0200 +++ b/PluginAiOllama.py Thu Aug 08 18:33:49 2024 +0200 @@ -127,6 +127,7 @@ "OllamaScheme": "http", "OllamaHost": "localhost", "OllamaPort": 11434, + "OllamaLocalPort": 11435, # port for locally started ollama server "OllamaHeartbeatInterval": 5, # 5 seconds heartbeat time; 0 = disabled "StreamingChatResponse": True, } @@ -268,7 +269,7 @@ @return the requested setting value @rtype Any """ - if key in ("OllamaPort", "OllamaHeartbeatInterval"): + if key in ("OllamaPort", "OllamaLocalPort", "OllamaHeartbeatInterval"): return int( Preferences.Prefs.settings.value( self.PreferencesKey + "/" + key, self.__defaults[key]