OllamaInterface/OllamaClient.py

changeset 4
7dd1b9cd3150
parent 3
ca28466a186d
child 5
6e8af43d537d
--- 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

eric ide

mercurial