aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/modules')
-rw-r--r--desktop/modules/assistant/AssistantModule.qml70
-rw-r--r--desktop/modules/assistant/AssistantPage.qml22
-rw-r--r--desktop/modules/assistant/README.md4
3 files changed, 82 insertions, 14 deletions
diff --git a/desktop/modules/assistant/AssistantModule.qml b/desktop/modules/assistant/AssistantModule.qml
index 5e7673c..a05556d 100644
--- a/desktop/modules/assistant/AssistantModule.qml
+++ b/desktop/modules/assistant/AssistantModule.qml
@@ -67,17 +67,24 @@ Module {
// not rebuilt on each keystroke (as envKeys).
readonly property var clouds: C.clouds(am.draft)
readonly property var savedClouds: C.clouds(am.text) ?? []
+ // Their models as the canvas names them, "provider:model". reduce, not
+ // flatMap: the shell's JS engine has no flatMap.
+ readonly property var cloudIds: am.savedClouds.reduce(
+ (a, c) => a.concat(c.models.map(m => c.name + ":" + m)), [])
property int cloudCount: 0
readonly property string cloudError: am.clouds ? C.cloudError(am.clouds) : ""
// name -> the provider's ids, or why they could not be fetched.
property var fetched: ({})
property string fetching: ""
- // Tool switches flipped but not saved: name -> wanted on.
+ // Tool switches flipped but not saved: name -> wanted on; and the same
+ // for each tool's cloud = true, which offers it to cloud chats.
property var toolsWanted: ({})
+ property var cloudWanted: ({})
// Fields holding a value that cannot be written; Save waits for none.
property var invalid: ({})
readonly property bool valid: Object.keys(am.invalid).length === 0 && am.cloudError === ""
dirty: am.draft !== am.base || Object.keys(am.toolsWanted).length > 0
+ || Object.keys(am.cloudWanted).length > 0
// The daemon's built-in values, from assistant.py --defaults.
property var defaults: ({})
// [{name, on, description}]
@@ -183,12 +190,14 @@ Module {
}
am.error = "";
const t = am.draft;
- const changed = t !== am.text || Object.keys(am.toolsWanted).length > 0;
- const tools = () => am.applyTools(Object.keys(am.toolsWanted), () => {
- if (changed) am.pending = true;
- am.listTools();
- if (done) done();
- });
+ const changed = t !== am.text || Object.keys(am.toolsWanted).length > 0
+ || Object.keys(am.cloudWanted).length > 0;
+ const tools = () => am.applyTools(Object.keys(am.toolsWanted), () =>
+ am.applyCloud(Object.keys(am.cloudWanted), () => {
+ if (changed) am.pending = true;
+ am.listTools();
+ if (done) done();
+ }));
if (t === am.text) { tools(); return; }
am.run([am.python, "-c", am.writer, am.cfgPath], t, (code, out, err) => {
if (code !== 0) { am.error = err.trim() || "config.toml could not be written"; return; }
@@ -201,6 +210,7 @@ Module {
am.error = "";
am.invalid = ({});
am.toolsWanted = ({});
+ am.cloudWanted = ({});
am.base = am.text;
am.draft = am.text;
}
@@ -246,9 +256,10 @@ Module {
+ "for p in glob.glob(sys.argv[1] + '/*.toml') + glob.glob(sys.argv[1] + '/*.toml.off'):\n"
+ " on = p.endswith('.toml')\n"
+ " name = os.path.basename(p).removesuffix('.off').removesuffix('.toml')\n"
- + " try:\n d = tomllib.load(open(p, 'rb')).get('description', '')\n"
- + " except Exception as e:\n d = f'unreadable: {e}'\n"
- + " out.append({'name': name, 'on': on, 'description': d})\n"
+ + " try:\n t = tomllib.load(open(p, 'rb'))\n"
+ + " d, cloud = t.get('description', ''), t.get('cloud') is True\n"
+ + " except Exception as e:\n d, cloud = f'unreadable: {e}', False\n"
+ + " out.append({'name': name, 'on': on, 'cloud': cloud, 'description': d})\n"
+ "print(json.dumps(sorted(out, key=lambda t: t['name'])))\n"
function listTools() {
@@ -264,6 +275,45 @@ Module {
if (!am.toolOn(t) === t.on) delete next[t.name]; else next[t.name] = !am.toolOn(t);
am.toolsWanted = next;
}
+ // Whether a tool goes to cloud chats in the draft, as toolOn.
+ function toolCloud(t) { return am.cloudWanted[t.name] ?? t.cloud; }
+ function toggleCloud(t) {
+ const next = Object.assign({}, am.cloudWanted);
+ if (!am.toolCloud(t) === t.cloud) delete next[t.name]; else next[t.name] = !am.toolCloud(t);
+ am.cloudWanted = next;
+ }
+
+ // Sets or drops a tool file's top-level cloud line, validated with
+ // tomllib and replaced atomically, like config.toml. The file is
+ // name.toml or, switched off, name.toml.off. Added as the first line:
+ // a top-level key is valid there whatever the file holds below.
+ readonly property string clouder: "import os, re, sys, tomllib\n"
+ + "p, on = sys.argv[1], sys.argv[2] == '1'\n"
+ + "if not os.path.exists(p):\n p += '.off'\n"
+ + "lines = open(p).read().split('\\n')\n"
+ + "head = next((i for i, l in enumerate(lines) if l.lstrip().startswith('[')), len(lines))\n"
+ + "lines = [l for i, l in enumerate(lines) if i >= head or not re.match(r'\\s*cloud\\s*=', l)]\n"
+ + "if on:\n lines.insert(0, 'cloud = true')\n"
+ + "t = '\\n'.join(lines)\n"
+ + "try:\n tomllib.loads(t)\n"
+ + "except tomllib.TOMLDecodeError as e:\n sys.exit(f'{os.path.basename(p)}: not valid TOML: {e}')\n"
+ + "with open(p + '.tmp', 'w') as f:\n f.write(t)\n"
+ + "os.replace(p + '.tmp', p)\n"
+
+ // One file at a time, after the renames, then done().
+ function applyCloud(names, done) {
+ if (names.length === 0) { done(); return; }
+ const name = names[0];
+ am.run([am.python, "-c", am.clouder, am.toolsDir + "/" + name + ".toml",
+ am.cloudWanted[name] ? "1" : "0"], null, (code, out, err) => {
+ if (code !== 0) { am.error = err.trim() || "could not change " + name; am.listTools(); return; }
+ const next = Object.assign({}, am.cloudWanted);
+ delete next[name];
+ am.cloudWanted = next;
+ am.applyCloud(names.slice(1), done);
+ });
+ }
+
// One rename at a time, then done(). -n never overwrites a file that
// already holds the other name.
function applyTools(names, done) {
diff --git a/desktop/modules/assistant/AssistantPage.qml b/desktop/modules/assistant/AssistantPage.qml
index 5878b3b..bf35763 100644
--- a/desktop/modules/assistant/AssistantPage.qml
+++ b/desktop/modules/assistant/AssistantPage.qml
@@ -233,7 +233,7 @@ Column {
// The saved cloud models: Prefer only, there is nothing to load.
Repeater {
- model: page.asst.savedClouds.flatMap(c => c.models.map(m => c.name + ":" + m))
+ model: page.asst.cloudIds
delegate: Rectangle {
id: cloudRow
@@ -640,7 +640,8 @@ Column {
Small {
width: page.width
wrapMode: Text.Wrap
- text: "What the model may call. Off renames the file to .toml.off, which the daemon skips."
+ text: "What the model may call. Off renames the file to .toml.off, which the daemon skips. "
+ + "Cloud also offers it to cloud chats, which then receive its results."
}
Repeater {
@@ -659,9 +660,24 @@ Column {
checked: page.asst.toolOn(toolRow.modelData)
onToggled: page.asst.toggleTool(toolRow.modelData)
}
+ // nf-fa-cloud, then the tool's cloud = true.
+ Switch {
+ id: cloudSw
+ anchors { right: parent.right; verticalCenter: parent.verticalCenter }
+ checked: page.asst.toolCloud(toolRow.modelData)
+ onToggled: page.asst.toggleCloud(toolRow.modelData)
+ }
+ Text {
+ id: cloudMark
+ anchors { right: cloudSw.left; rightMargin: 8; verticalCenter: parent.verticalCenter }
+ text: "\uf0c2"
+ color: page.asst.toolCloud(toolRow.modelData) ? Theme.text : Theme.subtext
+ font { family: Theme.iconFamily; pixelSize: Theme.fontSize - 4 }
+ }
Column {
id: toolText
- anchors { left: toolSw.right; leftMargin: 10; right: parent.right; verticalCenter: parent.verticalCenter }
+ anchors { left: toolSw.right; leftMargin: 10; right: cloudMark.left; rightMargin: 10
+ verticalCenter: parent.verticalCenter }
Text {
text: toolRow.modelData.name
color: page.asst.toolOn(toolRow.modelData) ? Theme.text : Theme.subtext
diff --git a/desktop/modules/assistant/README.md b/desktop/modules/assistant/README.md
index 9ee9736..a1ce840 100644
--- a/desktop/modules/assistant/README.md
+++ b/desktop/modules/assistant/README.md
@@ -13,7 +13,9 @@ are never copied here. A refused number is outlined and holds Save back.
The system prompt also has "Use the default". The tool environment
(`[env]`) is one row per key, with an add row below. A tool's switch, once
saved, renames `tools/<name>.toml`
-to `<name>.toml.off`, which the daemon's `*.toml` glob skips.
+to `<name>.toml.off`, which the daemon's `*.toml` glob skips. Its cloud switch sets or drops
+`cloud = true` at the top of the file, which offers the tool to cloud
+chats; the file is checked with `tomllib` and replaced atomically.
**Cloud** edits the daemon's `[[cloud]]` tables: name, URL and key
command per provider. Fetch saves the page, then runs `assistant.py