From 91c2014b7e5260c4ddb2b32a9d1e59b99313134c Mon Sep 17 00:00:00 2001
From: end-4 <97237370+end-4@users.noreply.github.com>
Date: Wed, 30 Jul 2025 09:46:42 +0700
Subject: [PATCH] ai: add mistral
---
.../ii/assets/icons/mistral-symbolic.svg | 95 ++++++++++++++
.../sidebarLeft/aiChat/MessageCodeBlock.qml | 2 +-
.config/quickshell/ii/services/Ai.qml | 112 +++++++++++++---
.../ii/services/ai/AiMessageData.qml | 1 +
.../ii/services/ai/MistralApiStrategy.qml | 124 ++++++++++++++++++
5 files changed, 318 insertions(+), 16 deletions(-)
create mode 100644 .config/quickshell/ii/assets/icons/mistral-symbolic.svg
create mode 100644 .config/quickshell/ii/services/ai/MistralApiStrategy.qml
diff --git a/.config/quickshell/ii/assets/icons/mistral-symbolic.svg b/.config/quickshell/ii/assets/icons/mistral-symbolic.svg
new file mode 100644
index 000000000..635b91db1
--- /dev/null
+++ b/.config/quickshell/ii/assets/icons/mistral-symbolic.svg
@@ -0,0 +1,95 @@
+
+
diff --git a/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageCodeBlock.qml b/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageCodeBlock.qml
index f2b9a1bd6..f8b0bac3e 100644
--- a/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageCodeBlock.qml
+++ b/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageCodeBlock.qml
@@ -252,7 +252,7 @@ ColumnLayout {
}
}
Loader {
- active: root.isCommandRequest && root.messageData.thinking
+ active: root.isCommandRequest && root.messageData.functionPending
visible: active
Layout.fillWidth: true
Layout.margins: 6
diff --git a/.config/quickshell/ii/services/Ai.qml b/.config/quickshell/ii/services/Ai.qml
index 7b6013188..cff1bcbb2 100644
--- a/.config/quickshell/ii/services/Ai.qml
+++ b/.config/quickshell/ii/services/Ai.qml
@@ -23,6 +23,7 @@ Singleton {
property Component aiModelComponent: AiModel {}
property Component geminiApiStrategy: GeminiApiStrategy {}
property Component openaiApiStrategy: OpenAiApiStrategy {}
+ property Component mistralApiStrategy: MistralApiStrategy {}
readonly property string interfaceRole: "interface"
readonly property string apiKeyEnvVarName: "API_KEY"
@@ -72,7 +73,7 @@ Singleton {
property var promptSubstitutions: {
"{DISTRO}": SystemInfo.distroName,
"{DATETIME}": `${DateTime.time}, ${DateTime.collapsedCalendarFormat}`,
- "{WINDOWCLASS}": ToplevelManager.activeToplevel.appId,
+ "{WINDOWCLASS}": ToplevelManager.activeToplevel?.appId ?? "Unknown",
"{DE}": `${SystemInfo.desktopEnvironment} (${SystemInfo.windowingSystem})`
}
@@ -131,12 +132,14 @@ Singleton {
"openai": {
"functions": [
{
- "type": "function",
- "name": "get_shell_config",
- "description": "Get the current shell configuration.",
+ "name": "switch_to_search_mode",
+ "description": "Search the web",
+ },
+ {
+ "name": "get_shell_config",
+ "description": "Get the desktop shell config file contents",
},
{
- "type": "function",
"name": "set_shell_config",
"description": "Set a field in the desktop graphical shell config file. Must only be used after `get_shell_config`.",
"parameters": {
@@ -151,10 +154,75 @@ Singleton {
"description": "The value to set, e.g. `true`"
}
},
- "required": ["key", "value"],
- "additionalProperties": false
+ "required": ["key", "value"]
}
- }
+ },
+ {
+ "name": "run_shell_command",
+ "description": "Run a shell command in bash and get its output. Use this only for quick commands that don't require user interaction. For commands that require interaction, ask the user to run manually instead.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "command": {
+ "type": "string",
+ "description": "The bash command to run",
+ },
+ },
+ "required": ["command"]
+ }
+ },
+ ],
+ "search": [],
+ "none": [],
+ },
+ "mistral": {
+ "functions": [
+ {
+ "type": "function",
+ "function": {
+ "name": "get_shell_config",
+ "description": "Get the desktop shell config file contents",
+ "parameters": {}
+ },
+ },
+ {
+ "type": "function",
+ "function": {
+ "name": "set_shell_config",
+ "description": "Set a field in the desktop graphical shell config file. Must only be used after `get_shell_config`.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "key": {
+ "type": "string",
+ "description": "The key to set, e.g. `bar.borderless`. MUST NOT BE GUESSED, use `get_shell_config` to see what keys are available before setting.",
+ },
+ "value": {
+ "type": "string",
+ "description": "The value to set, e.g. `true`"
+ }
+ },
+ "required": ["key", "value"]
+ }
+ }
+ },
+ {
+ "type": "function",
+ "function": {
+ "name": "run_shell_command",
+ "description": "Run a shell command in bash and get its output. Use this only for quick commands that don't require user interaction. For commands that require interaction, ask the user to run manually instead.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "command": {
+ "type": "string",
+ "description": "The bash command to run",
+ },
+ },
+ "required": ["command"]
+ }
+ },
+ },
],
"search": [],
"none": [],
@@ -232,6 +300,19 @@ Singleton {
"key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"),
"api_format": "gemini",
}),
+ "mistral-medium-3": aiModelComponent.createObject(this, {
+ "name": "Mistral Medium 3",
+ "icon": "mistral-symbolic",
+ "description": Translation.tr("Online | %1's model | Delivers fast, responsive and well-formatted answers. Disadvantages: not very eager to do stuff; might make up unknown function calls").arg("Mistral"),
+ "homepage": "https://mistral.ai/news/mistral-medium-3",
+ "endpoint": "https://api.mistral.ai/v1/chat/completions",
+ "model": "mistral-medium-2505",
+ "requires_key": true,
+ "key_id": "mistral",
+ "key_get_link": "https://console.mistral.ai/api-keys",
+ "key_get_description": Translation.tr("**Instructions**: Log into Mistral account, go to Keys on the sidebar, click Create new key"),
+ "api_format": "mistral",
+ }),
"openrouter-deepseek-r1": aiModelComponent.createObject(this, {
"name": "DeepSeek R1",
"icon": "deepseek-symbolic",
@@ -251,6 +332,7 @@ Singleton {
property var apiStrategies: {
"openai": openaiApiStrategy.createObject(this),
"gemini": geminiApiStrategy.createObject(this),
+ "mistral": mistralApiStrategy.createObject(this),
}
property ApiStrategy currentApiStrategy: apiStrategies[models[currentModelId]?.api_format || "openai"]
@@ -412,8 +494,8 @@ Singleton {
function addApiKeyAdvice(model) {
root.addMessage(
- Translation.tr('To set an API key, pass it with the command\n\nTo view the key, pass "get" with the command
\n\n### For %1:\n\n**Link**: %2\n\n%3')
- .arg(model.name).arg(model.key_get_link).arg(model.key_get_description ?? Translation.tr("No further instruction provided")),
+ Translation.tr('To set an API key, pass it with the %4 command\n\nTo view the key, pass "get" with the command
\n\n### For %1:\n\n**Link**: %2\n\n%3')
+ .arg(model.name).arg(model.key_get_link).arg(model.key_get_description ?? Translation.tr("No further instruction provided")).arg("/key"),
Ai.interfaceRole
);
}
@@ -659,14 +741,14 @@ Singleton {
}
function rejectCommand(message: AiMessageData) {
- if (!message.thinking) return;
- message.thinking = false; // User decided, no more "thinking"
+ if (!message.functionPending) return;
+ message.functionPending = false; // User decided, no more "thinking"
addFunctionOutputMessage(message.functionName, Translation.tr("Command rejected by user"))
}
function approveCommand(message: AiMessageData) {
- if (!message.thinking) return;
- message.thinking = false; // User decided, no more "thinking"
+ if (!message.functionPending) return;
+ message.functionPending = false; // User decided, no more "thinking"
const responseMessage = createFunctionOutputMessage(message.functionName, "", false);
const id = idForMessage(responseMessage);
@@ -726,7 +808,7 @@ Singleton {
const contentToAppend = `\n\n**Command execution request**\n\n\`\`\`command\n${args.command}\n\`\`\``;
message.rawContent += contentToAppend;
message.content += contentToAppend;
- message.thinking = true; // Use thinking to indicate the command is waiting for approval
+ message.functionPending = true; // Use thinking to indicate the command is waiting for approval
}
else root.addMessage(Translation.tr("Unknown function call: %1").arg(name), "assistant");
}
diff --git a/.config/quickshell/ii/services/ai/AiMessageData.qml b/.config/quickshell/ii/services/ai/AiMessageData.qml
index 5ec7fa336..023458d6e 100644
--- a/.config/quickshell/ii/services/ai/AiMessageData.qml
+++ b/.config/quickshell/ii/services/ai/AiMessageData.qml
@@ -16,5 +16,6 @@ QtObject {
property string functionName
property var functionCall
property string functionResponse
+ property bool functionPending: false
property bool visibleToUser: true
}
diff --git a/.config/quickshell/ii/services/ai/MistralApiStrategy.qml b/.config/quickshell/ii/services/ai/MistralApiStrategy.qml
new file mode 100644
index 000000000..dfcb950eb
--- /dev/null
+++ b/.config/quickshell/ii/services/ai/MistralApiStrategy.qml
@@ -0,0 +1,124 @@
+import QtQuick
+
+ApiStrategy {
+ property bool isReasoning: false
+
+ function buildEndpoint(model: AiModel): string {
+ // console.log("[AI] Endpoint: " + model.endpoint);
+ return model.endpoint;
+ }
+
+ function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real, tools: list) {
+ let baseData = {
+ "model": model.model,
+ "messages": [
+ {role: "system", content: systemPrompt},
+ ...messages.map(message => {
+ const hasFunctionCall = message.functionCall != undefined && message.functionName.length > 0
+ let messageData = {
+ "role": message.role,
+ "content": message.rawContent,
+ }
+ if (hasFunctionCall) {
+ if (message.functionResponse?.length > 0) {
+ messageData.name = message.functionName; // Does the func call also need this name? or just the func output?
+ messageData.role = "tool";
+ messageData.content = message.functionResponse;
+ messageData.tool_call_id = message.functionCall.id
+ }
+ }
+ return messageData
+ }),
+ ],
+ "stream": true,
+ "temperature": temperature,
+ "tools": tools,
+ };
+ // console.log("[AI] Request data: ", JSON.stringify(baseData, null, 2));
+ return model.extraParams ? Object.assign({}, baseData, model.extraParams) : baseData;
+ }
+
+ function buildAuthorizationHeader(apiKeyEnvVarName: string): string {
+ return `-H "Authorization: Bearer \$\{${apiKeyEnvVarName}\}"`;
+ }
+
+ function parseResponseLine(line, message) {
+ // Remove 'data: ' prefix if present and trim whitespace
+ let cleanData = line.trim();
+ if (cleanData.startsWith("data:")) {
+ cleanData = cleanData.slice(5).trim();
+ }
+
+ // Handle special cases
+ if (!cleanData || cleanData.startsWith(":")) return {};
+ if (cleanData === "[DONE]") {
+ return { finished: true };
+ }
+
+ // Real stuff
+ try {
+ const dataJson = JSON.parse(cleanData);
+ let newContent = "";
+
+ const responseContent = dataJson.choices[0]?.delta?.content || dataJson.message?.content;
+ const responseReasoning = dataJson.choices[0]?.delta?.reasoning || dataJson.choices[0]?.delta?.reasoning_content;
+
+ // Function call
+ if (dataJson.choices[0]?.delta?.tool_calls) {
+ const functionCall = dataJson.choices[0].delta.tool_calls[0];
+ const functionName = functionCall.function.name;
+ const functionArgs = JSON.parse(functionCall.function.arguments) || {}; // Args are given as string???
+ const functionId = functionCall.id;
+ const newContent = `\n\n[[ Function: ${functionName}(${JSON.stringify(functionArgs, null, 2)}) ]]\n`;
+ message.rawContent += newContent;
+ message.content += newContent;
+ message.functionName = functionName;
+ message.functionCall = functionName;
+ return { functionCall: { name: functionName, args: functionArgs, id: functionId } };
+ }
+
+ // Thinking?
+ if (responseContent && responseContent.length > 0) {
+ if (isReasoning) {
+ isReasoning = false;
+ const endBlock = "\n\n\n\n";
+ message.content += endBlock;
+ message.rawContent += endBlock;
+ }
+ newContent = responseContent;
+ } else if (responseReasoning && responseReasoning.length > 0) {
+ if (!isReasoning) {
+ isReasoning = true;
+ const startBlock = "\n\n\n\n";
+ message.rawContent += startBlock;
+ message.content += startBlock;
+ }
+ newContent = responseReasoning;
+ }
+
+ // Text
+ message.content += newContent;
+ message.rawContent += newContent;
+
+ if (`dataJson`.done) {
+ return { finished: true };
+ }
+
+ } catch (e) {
+ console.log("[AI] Mistral: Could not parse line: ", e);
+ message.rawContent += line;
+ message.content += line;
+ }
+
+ return {};
+ }
+
+ function onRequestFinished(message) {
+ return {};
+ }
+
+ function reset() {
+ isReasoning = false;
+ }
+
+}