--- a/OllamaInterface/OllamaClient.py Sun Aug 04 16:57:01 2024 +0200 +++ b/OllamaInterface/OllamaClient.py Mon Aug 05 18:37:16 2024 +0200 @@ -12,8 +12,16 @@ import enum import json -from PyQt6.QtCore import pyqtSignal, QObject, QUrl -from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply +from PyQt6.QtCore import ( + QCoreApplication, + QObject, + QThread, + QTimer, + QUrl, + pyqtSignal, + pyqtSlot, +) +from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest from eric7.EricNetwork.EricNetworkProxyFactory import proxyAuthenticationRequired @@ -22,6 +30,7 @@ """ Class defining the various client states. """ + Waiting = 0 Requesting = 1 Receiving = 2 @@ -32,7 +41,7 @@ """ Class implementing the 'ollama' client. - @signal replyReceived(content:str, role:str) emitted after a response from the + @signal replyReceived(content:str, role:str) emitted after a response from the 'ollama' server was received @signal modelsList(modelNames:list[str]) emitted after the list of model names was obtained from the 'ollama' server @@ -43,9 +52,13 @@ execution details @signal pullStatus(msg:str, id:str, total:int, completed:int) emitted to indicate the status of a pull request as reported by the 'ollama' server + @signal serverVersion(version:str) emitted after the server version was obtained + from the 'ollama' server @signal finished() emitted to indicate the completion of a request @signal errorOccurred(error:str) emitted to indicate a network error occurred while processing the request + @signal serverStateChanged(ok:bool) emitted to indicate a change of the server + responsiveness """ replyReceived = pyqtSignal(str, str) @@ -53,8 +66,10 @@ detailedModelsList = pyqtSignal(list) runningModelsList = pyqtSignal(list) pullStatus = pyqtSignal(str, str, int, int) + serverVersion = pyqtSignal(str) finished = pyqtSignal() errorOccurred = pyqtSignal(str) + serverStateChanged = pyqtSignal(bool) def __init__(self, plugin, parent=None): """ @@ -75,8 +90,17 @@ proxyAuthenticationRequired ) + self.__serverResponding = False + self.__heartbeatTimer = QTimer(self) + self.__heartbeatTimer.timeout.connect(self.__periodicHeartbeat) + self.__state = OllamaClientState.Waiting + self.__serverResponding = False # start with a faulty state + + self.__plugin.preferencesChanged.connect(self.__setHeartbeatTimer) + self.__setHeartbeatTimer() + def chat(self, model, messages): """ Public method to request a chat completion from the 'ollama' server. @@ -188,7 +212,6 @@ Public method to request a list of models available locally from the 'ollama' server. """ - # TODO: not implemented yet self.__sendRequest("tags", processResponse=self.__processModelsList) def __processModelsList(self, response): @@ -203,16 +226,13 @@ for model in response["models"]: name = model["name"] if name: - models.append(name) + models.append(name.replace(":latest", "")) self.modelsList.emit(models) def listDetails(self): """ Public method to request a list of models available locally from the 'ollama' server with some model details. - - @return list of available models - @rtype list of dict """ # TODO: not implemented yet self.__sendRequest("tags", processResponse=self.__processDetailedModelsList) @@ -244,7 +264,7 @@ def listRunning(self): """ - Public method to request a list of running models from the 'ollama' server + Public method to request a list of running models from the 'ollama' server. """ # TODO: not implemented yet self.__sendRequest("ps", processResponse=self.__processRunningModelsList) @@ -288,10 +308,26 @@ ) self.runningModelsList.emit(models) + def version(self): + """ + Public method to request the version from the 'ollama' server. + """ + self.__sendRequest("version", processResponse=self.__processVersion) + + def __processVersion(self, response): + """ + Private method to process the version response of the 'ollama' server. + + @param response dictionary containing the version response + @type dict + """ + with contextlib.suppress(KeyError): + self.serverVersion.emit(response["version"]) + def state(self): """ Public method to get the current client state. - + @return current client state @rtype OllamaClientState """ @@ -326,9 +362,9 @@ QNetworkRequest.KnownHeaders.ContentTypeHeader, "application/json" ) jsonData = json.dumps(data).encode("utf-8") - reply = self.__networkManager.post(request=request, data=jsonData) + reply = self.__networkManager.post(request, jsonData) else: - reply = self.__networkManager.get(request=request) + reply = self.__networkManager.get(request) reply.finished.connect(lambda: self.__replyFinished(reply)) reply.errorOccurred.connect(lambda error: self.__errorOccurred(error, reply)) @@ -383,3 +419,49 @@ data = json.loads(buffer) if data and processResponse: processResponse(data) + + def heartbeat(self): + """ + Public method to check, if the 'ollama' server has started and is responsive. + + @return flag indicating a responsive 'ollama' server + @rtype bool + """ + ollamaUrl = QUrl( + "{0}://{1}:{2}/".format( + self.__plugin.getPreferences("OllamaScheme"), + self.__plugin.getPreferences("OllamaHost"), + self.__plugin.getPreferences("OllamaPort"), + ) + ) + request = QNetworkRequest(ollamaUrl) + reply = self.__networkManager.head(request) + while not reply.isFinished(): + QCoreApplication.processEvents() + QThread.msleep(100) + + reply.deleteLater() + + return reply.error() == QNetworkReply.NetworkError.NoError + + @pyqtSlot() + def __setHeartbeatTimer(self): + """ + Private slot to configure the heartbeat timer. + """ + interval = self.__plugin.getPreferences("OllamaHeartbeatInterval") + if interval: + self.__heartbeatTimer.setInterval(interval * 1000) # interval in ms + self.__heartbeatTimer.start() + else: + self.__heartbeatTimer.stop() + + @pyqtSlot() + def __periodicHeartbeat(self): + """ + Private slot to do a periodic check of the 'ollama' server responsiveness. + """ + responding = self.heartbeat() + if responding != self.__serverResponding: + self.serverStateChanged.emit(responding) + self.__serverResponding = responding