mirror of
https://github.com/danbulant/dots-hyprland
synced 2026-05-19 04:08:48 +00:00
ai: add mistral
This commit is contained in:
parent
3018ad16b1
commit
91c2014b7e
5 changed files with 318 additions and 16 deletions
95
.config/quickshell/ii/assets/icons/mistral-symbolic.svg
Normal file
95
.config/quickshell/ii/assets/icons/mistral-symbolic.svg
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="19.856001"
|
||||
height="19.856001"
|
||||
viewBox="0 0 128.071 128.07101"
|
||||
version="1.1"
|
||||
xml:space="preserve"
|
||||
style="clip-rule:evenodd;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"
|
||||
id="svg10"
|
||||
sodipodi:docname="mistral-symbolic.svg"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs10" /><sodipodi:namedview
|
||||
id="namedview10"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="14.139535"
|
||||
inkscape:cx="13.366776"
|
||||
inkscape:cy="8.1332237"
|
||||
inkscape:window-width="1703"
|
||||
inkscape:window-height="1028"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g10" /><g
|
||||
id="g10"
|
||||
transform="translate(2.927246e-6,18.722004)"><rect
|
||||
x="18.292"
|
||||
y="0"
|
||||
width="18.292999"
|
||||
height="18.122999"
|
||||
style="fill:#999999;fill-rule:nonzero"
|
||||
id="rect1" /><rect
|
||||
x="91.473"
|
||||
y="0"
|
||||
width="18.292999"
|
||||
height="18.122999"
|
||||
style="fill:#999999;fill-rule:nonzero"
|
||||
id="rect2" /><rect
|
||||
x="18.292"
|
||||
y="18.121"
|
||||
width="36.585999"
|
||||
height="18.122999"
|
||||
style="fill:#666666;fill-rule:nonzero"
|
||||
id="rect3" /><rect
|
||||
x="73.181"
|
||||
y="18.121"
|
||||
width="36.585999"
|
||||
height="18.122999"
|
||||
style="fill:#666666;fill-rule:nonzero"
|
||||
id="rect4" /><rect
|
||||
x="18.292"
|
||||
y="36.243"
|
||||
width="91.475998"
|
||||
height="18.122"
|
||||
style="fill:#4d4d4d;fill-rule:nonzero"
|
||||
id="rect5" /><rect
|
||||
x="18.292"
|
||||
y="54.369999"
|
||||
width="18.292999"
|
||||
height="18.122999"
|
||||
style="fill:#333333;fill-rule:nonzero"
|
||||
id="rect6" /><rect
|
||||
x="54.882999"
|
||||
y="54.369999"
|
||||
width="18.292999"
|
||||
height="18.122999"
|
||||
style="fill:#333333;fill-rule:nonzero"
|
||||
id="rect7" /><rect
|
||||
x="91.473"
|
||||
y="54.369999"
|
||||
width="18.292999"
|
||||
height="18.122999"
|
||||
style="fill:#333333;fill-rule:nonzero"
|
||||
id="rect8" /><rect
|
||||
x="0"
|
||||
y="72.503998"
|
||||
width="54.889999"
|
||||
height="18.122999"
|
||||
style="fill:#1a1a1a;fill-rule:nonzero"
|
||||
id="rect9" /><rect
|
||||
x="73.181"
|
||||
y="72.503998"
|
||||
width="54.889999"
|
||||
height="18.122999"
|
||||
style="fill:#1a1a1a;fill-rule:nonzero"
|
||||
id="rect10" /></g></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<br/>\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("<i>No further instruction provided</i>")),
|
||||
Translation.tr('To set an API key, pass it with the %4 command\n\nTo view the key, pass "get" with the command<br/>\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("<i>No further instruction provided</i>")).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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,5 +16,6 @@ QtObject {
|
|||
property string functionName
|
||||
property var functionCall
|
||||
property string functionResponse
|
||||
property bool functionPending: false
|
||||
property bool visibleToUser: true
|
||||
}
|
||||
|
|
|
|||
124
.config/quickshell/ii/services/ai/MistralApiStrategy.qml
Normal file
124
.config/quickshell/ii/services/ai/MistralApiStrategy.qml
Normal file
|
|
@ -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<var>) {
|
||||
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</think>\n\n";
|
||||
message.content += endBlock;
|
||||
message.rawContent += endBlock;
|
||||
}
|
||||
newContent = responseContent;
|
||||
} else if (responseReasoning && responseReasoning.length > 0) {
|
||||
if (!isReasoning) {
|
||||
isReasoning = true;
|
||||
const startBlock = "\n\n<think>\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;
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue