mirror of
https://github.com/danbulant/dots-hyprland
synced 2026-05-25 12:52:09 +00:00
ai: gemini: configurator
This commit is contained in:
parent
35bd46faea
commit
bf22194182
5 changed files with 252 additions and 18 deletions
|
|
@ -5,7 +5,7 @@ pragma ComponentBehavior: Bound
|
|||
|
||||
Singleton {
|
||||
property QtObject ai: QtObject {
|
||||
property string systemPrompt: qsTr("Use casual tone. No user knowledge is to be assumed except basic Linux literacy. Be brief and concise: When explaining concepts, use bullet points (prefer minus sign (-) over asterisk (*)) and highlight keywords in bold to pinpoint the main concepts instead of long paragraphs. You are also encouraged to split your response with h2 headers, each header title beginning with an emoji, like `## 🐧 Linux`.")
|
||||
property string systemPrompt: qsTr("Use casual tone. No user knowledge is to be assumed except basic Linux literacy. Be brief and concise: When explaining concepts, use bullet points (prefer minus sign (-) over asterisk (*)) and highlight keywords in bold to pinpoint the main concepts instead of long paragraphs. You are also encouraged to split your response with h2 headers, each header title beginning with an emoji, like `## 🐧 Linux`. When making changes to the user's config, you must get the config to know what values there are before setting.")
|
||||
}
|
||||
|
||||
property QtObject appearance: QtObject {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ pragma ComponentBehavior: Bound
|
|||
|
||||
Singleton {
|
||||
property QtObject ai: QtObject {
|
||||
property string model: "gemini-2.0-flash-search"
|
||||
property string model
|
||||
}
|
||||
|
||||
property QtObject sidebar: QtObject {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ pragma Singleton
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "root:/modules/common/functions/string_utils.js" as StringUtils
|
||||
import "root:/modules/common/functions/object_utils.js" as ObjectUtils
|
||||
import "root:/modules/common"
|
||||
import Quickshell;
|
||||
import Quickshell.Io;
|
||||
|
|
@ -23,6 +24,7 @@ Singleton {
|
|||
property var messageByID: ({})
|
||||
readonly property var apiKeys: KeyringStorage.keyringData?.apiKeys ?? {}
|
||||
readonly property var apiKeysLoaded: KeyringStorage.loaded
|
||||
property var postResponseHook
|
||||
|
||||
function idForMessage(message) {
|
||||
// Generate a unique ID using timestamp and random value
|
||||
|
|
@ -48,7 +50,7 @@ Singleton {
|
|||
// - extraParams: Extra parameters to be passed to the model. This is a JSON object.
|
||||
property var models: {
|
||||
"gemini-2.0-flash-search": {
|
||||
"name": "Gemini 2.0 Flash",
|
||||
"name": "Gemini 2.0 Flash (Search)",
|
||||
"icon": "google-gemini-symbolic",
|
||||
"description": qsTr("Online | Google's model\nGives up-to-date information with search."),
|
||||
"homepage": "https://aistudio.google.com",
|
||||
|
|
@ -65,8 +67,53 @@ Singleton {
|
|||
},
|
||||
]
|
||||
},
|
||||
"gemini-2.5-flash-preview-05-20": {
|
||||
"name": "Gemini 2.5 Flash (preview)",
|
||||
"gemini-2.0-flash-tools": {
|
||||
"name": "Gemini 2.0 Flash (Tools)",
|
||||
"icon": "google-gemini-symbolic",
|
||||
"description": qsTr("Experimental | Online | Google's model\nCan do a little more but doesn't search quickly"),
|
||||
"homepage": "https://aistudio.google.com",
|
||||
"endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent",
|
||||
"model": "gemini-2.0-flash",
|
||||
"requires_key": true,
|
||||
"key_id": "gemini",
|
||||
"key_get_link": "https://aistudio.google.com/app/apikey",
|
||||
"key_get_description": qsTr("**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",
|
||||
"tools": [
|
||||
{
|
||||
"functionDeclarations": [
|
||||
{
|
||||
"name": "switch_to_search_mode",
|
||||
"description": "Search the web",
|
||||
},
|
||||
{
|
||||
"name": "get_shell_config",
|
||||
"description": "Get the desktop shell config file contents",
|
||||
},
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gemini-2.5-flash-search": {
|
||||
"name": "Gemini 2.5 Flash (Search)",
|
||||
"icon": "google-gemini-symbolic",
|
||||
"description": qsTr("Online | Google's model\nGives up-to-date information with search."),
|
||||
"homepage": "https://aistudio.google.com",
|
||||
|
|
@ -83,6 +130,51 @@ Singleton {
|
|||
},
|
||||
]
|
||||
},
|
||||
"gemini-2.5-flash-tools": {
|
||||
"name": "Gemini 2.5 Flash (Tools)",
|
||||
"icon": "google-gemini-symbolic",
|
||||
"description": qsTr("Experimental | Online | Google's model\nCan do a little more but doesn't search quickly"),
|
||||
"homepage": "https://aistudio.google.com",
|
||||
"endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:streamGenerateContent",
|
||||
"model": "gemini-2.5-flash-preview-05-20",
|
||||
"requires_key": true,
|
||||
"key_id": "gemini",
|
||||
"key_get_link": "https://aistudio.google.com/app/apikey",
|
||||
"key_get_description": qsTr("**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",
|
||||
"tools": [
|
||||
{
|
||||
"functionDeclarations": [
|
||||
{
|
||||
"name": "switch_to_search_mode",
|
||||
"description": "Search the web",
|
||||
},
|
||||
{
|
||||
"name": "get_shell_config",
|
||||
"description": "Get the desktop shell config file contents",
|
||||
},
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"openrouter-llama4-maverick": {
|
||||
"name": "Llama 4 Maverick",
|
||||
"icon": "ollama-symbolic",
|
||||
|
|
@ -109,7 +201,7 @@ Singleton {
|
|||
},
|
||||
}
|
||||
property var modelList: Object.keys(root.models)
|
||||
property var currentModelId: PersistentStates.ai.model
|
||||
property var currentModelId: PersistentStates?.ai?.model || modelList[0]
|
||||
|
||||
Component.onCompleted: {
|
||||
setModel(currentModelId, false); // Do necessary setup for model
|
||||
|
|
@ -205,7 +297,7 @@ Singleton {
|
|||
modelId = modelId.toLowerCase()
|
||||
if (modelList.indexOf(modelId) !== -1) {
|
||||
PersistentStateManager.setState("ai.model", modelId);
|
||||
if (feedback) root.addMessage(StringUtils.format(StringUtils.format("Model set to {0}"), models[modelId].name), Ai.interfaceRole)
|
||||
if (feedback) root.addMessage(StringUtils.format(StringUtils.format("Model set to {0}"), models[modelId].name), root.interfaceRole)
|
||||
if (models[modelId].requires_key) {
|
||||
// If key not there show advice
|
||||
if (root.apiKeysLoaded && (!root.apiKeys[models[modelId].key_id] || root.apiKeys[models[modelId].key_id].length === 0)) {
|
||||
|
|
@ -271,12 +363,45 @@ Singleton {
|
|||
return model.endpoint;
|
||||
}
|
||||
|
||||
function markDone() {
|
||||
requester.message.done = true;
|
||||
if (root.postResponseHook) {
|
||||
root.postResponseHook();
|
||||
root.postResponseHook = null; // Reset hook after use
|
||||
}
|
||||
}
|
||||
|
||||
function buildGeminiRequestData(model, messages) {
|
||||
let baseData = {
|
||||
"contents": messages.filter(message => (message.role != Ai.interfaceRole)).map(message => ({
|
||||
"role": message.role,
|
||||
"parts": [{ text: message.content }]
|
||||
})),
|
||||
"contents": messages.filter(message => (message.role != Ai.interfaceRole)).map(message => {
|
||||
if (message.functionCall != undefined && message.functionCall.length > 0) {
|
||||
return {
|
||||
"role": message.role,
|
||||
"parts": [{
|
||||
functionCall: {
|
||||
"name": message.functionName,
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
if (message.functionResponse != undefined && message.functionResponse.length > 0) {
|
||||
return {
|
||||
"role": message.role,
|
||||
"parts": [{
|
||||
functionResponse: {
|
||||
"name": message.functionName,
|
||||
"response": { "content": message.functionResponse }
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
return {
|
||||
"role": message.role,
|
||||
"parts": [{
|
||||
text: message.content,
|
||||
}]
|
||||
}
|
||||
}),
|
||||
"tools": [
|
||||
...model.tools,
|
||||
],
|
||||
|
|
@ -315,6 +440,7 @@ Singleton {
|
|||
const endpoint = (apiFormat === "gemini") ? buildGeminiEndpoint(model) : buildOpenAIEndpoint(model);
|
||||
const messageArray = root.messageIDs.map(id => root.messageByID[id]);
|
||||
const data = (apiFormat === "gemini") ? buildGeminiRequestData(model, messageArray) : buildOpenAIRequestData(model, messageArray);
|
||||
// console.log("REQUEST DATA: ", JSON.stringify(data, null, 2));
|
||||
|
||||
let requestHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -355,9 +481,20 @@ Singleton {
|
|||
}
|
||||
|
||||
function parseGeminiBuffer() {
|
||||
// console.log("BUFFER DATA: ", requester.geminiBuffer);
|
||||
console.log("BUFFER DATA: ", requester.geminiBuffer);
|
||||
try {
|
||||
if (requester.geminiBuffer.length === 0) return;
|
||||
const dataJson = JSON.parse(requester.geminiBuffer);
|
||||
// Function call handling
|
||||
if (dataJson.candidates[0]?.content?.parts[0]?.functionCall) {
|
||||
const functionCall = dataJson.candidates[0]?.content?.parts[0]?.functionCall;
|
||||
requester.message.functionName = functionCall.name;
|
||||
requester.message.functionCall = functionCall.name;
|
||||
requester.message.content += `\n\n[[ Function: ${functionCall.name}(${JSON.stringify(functionCall.args, null, 2)}) ]]\n`;
|
||||
root.handleGeminiFunctionCall(functionCall.name, functionCall.args);
|
||||
return
|
||||
}
|
||||
// Normal text response
|
||||
const responseContent = dataJson.candidates[0]?.content?.parts[0]?.text
|
||||
requester.message.content += responseContent;
|
||||
const annotationSources = dataJson.candidates[0]?.groundingMetadata?.groundingChunks?.map(chunk => {
|
||||
|
|
@ -394,7 +531,7 @@ Singleton {
|
|||
} else if (line == "]") {
|
||||
requester.geminiBuffer += line.slice(0, -1).trim();
|
||||
parseGeminiBuffer();
|
||||
requester.message.done = true;
|
||||
requester.markDone();
|
||||
} else if (line.startsWith(",")) { // end of one entry
|
||||
parseGeminiBuffer();
|
||||
} else {
|
||||
|
|
@ -412,7 +549,7 @@ Singleton {
|
|||
if (!cleanData || cleanData.startsWith(":")) return;
|
||||
|
||||
if (cleanData === "[DONE]") {
|
||||
requester.message.done = true;
|
||||
requester.markDone();
|
||||
return;
|
||||
}
|
||||
const dataJson = JSON.parse(cleanData);
|
||||
|
|
@ -438,7 +575,7 @@ Singleton {
|
|||
|
||||
requester.message.content += newContent;
|
||||
|
||||
if (dataJson.done) requester.message.done = true;
|
||||
if (dataJson.done) requester.markDone();
|
||||
}
|
||||
|
||||
stdout: SplitParser {
|
||||
|
|
@ -467,7 +604,7 @@ Singleton {
|
|||
}
|
||||
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
requester.message.done = true;
|
||||
requester.markDone();
|
||||
if (requester.apiFormat == "gemini") requester.parseGeminiBuffer();
|
||||
|
||||
try { // to parse full response into json for error handling
|
||||
|
|
@ -490,4 +627,60 @@ Singleton {
|
|||
requester.makeRequest();
|
||||
}
|
||||
|
||||
function addFunctionOutputMessage(name, output) {
|
||||
const aiMessage = aiMessageComponent.createObject(root, {
|
||||
"role": "user",
|
||||
"content": `[[ Output of ${name} ]]`,
|
||||
"functionName": name,
|
||||
"functionResponse": output,
|
||||
"thinking": false,
|
||||
"done": true,
|
||||
});
|
||||
console.log("Adding function output message: ", JSON.stringify(aiMessage));
|
||||
const id = idForMessage(aiMessage);
|
||||
root.messageIDs = [...root.messageIDs, id];
|
||||
root.messageByID[id] = aiMessage;
|
||||
}
|
||||
|
||||
function buildGeminiFunctionOutput(name, output) {
|
||||
const functionResponsePart = {
|
||||
"name": name,
|
||||
"response": { "content": output }
|
||||
}
|
||||
return {
|
||||
"role": "user",
|
||||
"parts": [{
|
||||
functionResponse: functionResponsePart,
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
function handleGeminiFunctionCall(name, args) {
|
||||
if (name === "switch_to_search_mode") {
|
||||
if (root.currentModelId === "gemini-2.5-flash-tools") {
|
||||
root.setModel("gemini-2.5-flash-search", false);
|
||||
root.postResponseHook = () => root.setModel("gemini-2.5-flash-tools", false);
|
||||
} else if (root.currentModelId === "gemini-2.0-flash-tools") {
|
||||
root.setModel("gemini-2.0-flash-search", false);
|
||||
root.postResponseHook = () => root.setModel("gemini-2.0-flash-tools", false);
|
||||
}
|
||||
addFunctionOutputMessage(name, qsTr("Switched to search mode. Continue with the user's request."))
|
||||
requester.makeRequest();
|
||||
} else if (name === "get_shell_config") {
|
||||
const configJson = ObjectUtils.toPlainObject(ConfigOptions)
|
||||
addFunctionOutputMessage(name, JSON.stringify(configJson));
|
||||
requester.makeRequest();
|
||||
} else if (name === "set_shell_config") {
|
||||
if (!args.key || !args.value) {
|
||||
addFunctionOutputMessage(name, qsTr("Invalid arguments. Must provide `key` and `value`."));
|
||||
return;
|
||||
}
|
||||
const key = args.key;
|
||||
const value = args.value;
|
||||
ConfigLoader.setLiveConfigValue(key, value);
|
||||
ConfigLoader.saveConfig();
|
||||
}
|
||||
else root.addMessage(qsTr("Unknown function call: {0}"), "assistant");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,4 +12,7 @@ QtObject {
|
|||
property bool done: false
|
||||
property var annotations: []
|
||||
property var annotationSources: []
|
||||
property string functionName
|
||||
property string functionCall
|
||||
property string functionResponse
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
|
|||
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/functions/file_utils.js" as FileUtils
|
||||
import "root:/modules/common/functions/string_utils.js" as StringUtils
|
||||
import "root:/modules/common/functions/object_utils.js" as ObjectUtils
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
|
@ -38,9 +39,47 @@ Singleton {
|
|||
console.error("[ConfigLoader] Error reading file:", e);
|
||||
Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration failed to load")}" "${root.filePath}"`)
|
||||
return;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function setLiveConfigValue(nestedKey, value) {
|
||||
let keys = nestedKey.split(".");
|
||||
let obj = ConfigOptions;
|
||||
let parents = [obj];
|
||||
|
||||
// Traverse and collect parent objects
|
||||
for (let i = 0; i < keys.length - 1; ++i) {
|
||||
if (!obj[keys[i]] || typeof obj[keys[i]] !== "object") {
|
||||
obj[keys[i]] = {};
|
||||
}
|
||||
obj = obj[keys[i]];
|
||||
parents.push(obj);
|
||||
}
|
||||
|
||||
// Convert value to correct type using JSON.parse when safe
|
||||
let convertedValue = value;
|
||||
if (typeof value === "string") {
|
||||
let trimmed = value.trim();
|
||||
if (trimmed === "true" || trimmed === "false" || !isNaN(Number(trimmed))) {
|
||||
try {
|
||||
convertedValue = JSON.parse(trimmed);
|
||||
} catch (e) {
|
||||
convertedValue = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(parents.join("."));
|
||||
console.log(`[ConfigLoader] Setting live config value: ${nestedKey} = ${convertedValue}`);
|
||||
obj[keys[keys.length - 1]] = convertedValue;
|
||||
}
|
||||
|
||||
function saveConfig() {
|
||||
const plainConfig = ObjectUtils.toPlainObject(ConfigOptions)
|
||||
Hyprland.dispatch(`exec echo '${StringUtils.shellSingleQuoteEscape(JSON.stringify(plainConfig, null, 2))}' > '${root.filePath}'`)
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: delayedFileRead
|
||||
interval: ConfigOptions.hacks.arbitraryRaceConditionDelay
|
||||
|
|
@ -67,8 +106,7 @@ Singleton {
|
|||
onLoadFailed: (error) => {
|
||||
if(error == FileViewError.FileNotFound) {
|
||||
console.log("[ConfigLoader] File not found, creating new file.")
|
||||
const plainConfig = ObjectUtils.toPlainObject(ConfigOptions)
|
||||
configFileView.setText(JSON.stringify(plainConfig, null, 2))
|
||||
root.saveConfig()
|
||||
Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration created")}" "${root.filePath}"`)
|
||||
} else {
|
||||
Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration failed to load")}" "${root.filePath}"`)
|
||||
|
|
|
|||
Loading…
Reference in a new issue