aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/assistant/AssistantModule.qml
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/modules/assistant/AssistantModule.qml')
-rw-r--r--desktop/modules/assistant/AssistantModule.qml57
1 files changed, 57 insertions, 0 deletions
diff --git a/desktop/modules/assistant/AssistantModule.qml b/desktop/modules/assistant/AssistantModule.qml
index e89fe96..a85b2b3 100644
--- a/desktop/modules/assistant/AssistantModule.qml
+++ b/desktop/modules/assistant/AssistantModule.qml
@@ -52,6 +52,17 @@ Module {
property var tools: []
// A write since the last restart, which the daemon has not read yet.
property bool pending: false
+
+ // The router's models, [{id, status}], status as it reports it:
+ // "loaded", "loading", "unloaded" and so on. Empty when it is down.
+ property var models: []
+ // The id being loaded, "" when none is.
+ property string loading: ""
+ property string modelError: ""
+ // The router answers /models and /models/load on the server's root, and
+ // the OpenAI API under /v1, which is what the daemon's llm points at.
+ readonly property string llm: am.values.llm ?? am.defaults.llm ?? "http://localhost:8181/v1"
+ readonly property string routerRoot: am.llm.replace(/\/v1\/?$/, "")
property bool restarting: false
property string status: ""
property string error: ""
@@ -59,10 +70,12 @@ Module {
searchItems: am.keys.map(k => ({ text: am.labels[k], key: "key:" + k }))
.concat(am.envList.map(e => ({ text: e.key + " = " + e.value, key: "env:" + e.key })))
.concat(am.tools.map(t => ({ text: "tool: " + t.name, key: "tool:" + t.name })))
+ .concat(am.models.map(m => ({ text: "model: " + m.id, key: "model:" + m.id })))
function refresh() {
fv.reload();
am.listTools();
+ am.listModels();
if (Object.keys(am.defaults).length === 0)
am.run([am.python, am.daemonDir + "/assistant.py", "--defaults"], null, (code, out) => {
try { if (code === 0) am.defaults = JSON.parse(out); } catch (e) {}
@@ -156,6 +169,50 @@ Module {
});
}
+ function listModels(done) {
+ am.run(["curl", "-sf", "-m", "3", am.llm + "/models"], null, (code, out) => {
+ try {
+ am.models = code === 0 ? JSON.parse(out).data.map(m => ({ id: m.id, status: m.status?.value ?? "" })) : [];
+ } catch (e) { am.models = []; }
+ if (done) done();
+ });
+ }
+
+ // The router keeps one model resident, so this swaps out whatever is
+ // loaded, for every client: it runs only on a click. The id becomes the
+ // preferred one, and the list is polled until the router reports it
+ // loaded, since the request may return before the load finishes.
+ function loadModel(id) {
+ Prefs.setAssistantModel(id);
+ am.modelError = "";
+ am.loading = id;
+ am.run(["curl", "-s", "-m", "600", "-X", "POST", "-H", "Content-Type: application/json",
+ "-d", JSON.stringify({ model: id }), am.routerRoot + "/models/load"], null, (code, out) => {
+ let err = code === 0 ? "" : "the router did not answer";
+ try {
+ const e = JSON.parse(out).error;
+ // Already running is the outcome asked for, not a failure.
+ if (e && e.message !== "model is already running") err = e.message;
+ } catch (x) {}
+ if (err) { am.modelError = `${id}: ${err}`; am.loading = ""; am.listModels(); }
+ else { poll.left = 300; poll.start(); }
+ });
+ }
+ property Timer poll: Timer {
+ property int left: 0
+ interval: 1000
+ repeat: true
+ onTriggered: am.listModels(() => {
+ const m = am.models.find(x => x.id === am.loading);
+ if (m && m.status === "loaded") { am.loading = ""; am.poll.stop(); }
+ else if (--am.poll.left <= 0) {
+ am.modelError = `${am.loading} did not finish loading`;
+ am.loading = "";
+ am.poll.stop();
+ }
+ })
+ }
+
function restart() {
am.restarting = true;
am.run([am.daemonDir + "/assistant.sh", "restart"], null, (code, out) => {