Files
illogical-impulse/.config/ags/services/gpt.js
T
2025-07-05 16:15:01 +08:00

338 lines
14 KiB
JavaScript

import Service from 'resource:///com/github/Aylur/ags/service.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Soup from 'gi://Soup?version=3.0';
import { fileExists } from '../modules/.miscutils/files.js';
function guessModelLogo(model) {
if (model.includes("llama")) return "ollama-symbolic";
if (model.includes("gemma")) return "google-gemini-symbolic";
if (model.includes("deepseek")) return "deepseek-symbolic";
if (/^phi\d*:/i.test(model)) return "microsoft-symbolic";
return "ollama-symbolic";
}
function guessModelName(model) {
const replaced = model.replace(/-/g, ' ').replace(/:/g, ' ');
const words = replaced.split(' ');
words[words.length - 1] = words[words.length - 1].replace(/(\d+)b$/, (_, num) => `${num}B`)
words[words.length - 1] = `[${words[words.length - 1]}]`; // Surround the last word with square brackets
const result = words.join(' ');
return result.charAt(0).toUpperCase() + result.slice(1); // Capitalize the first letter
}
const PROVIDERS = Object.assign({
"ollama_llama_3_2": {
"name": "Ollama - Llama 3.2",
"logo_name": "ollama-symbolic",
"description": getString('Ollama - Llama-3.2'),
"base_url": 'http://localhost:11434/v1/chat/completions',
"key_get_url": "",
"requires_key": false,
"key_file": "ollama_key.txt",
"model": "llama3.2",
},
"openrouter": {
"name": "OpenRouter (Llama-3-70B)",
"logo_name": "openrouter-symbolic",
"description": getString('A unified interface for LLMs'),
"base_url": "https://openrouter.ai/api/v1/chat/completions",
"key_get_url": "https://openrouter.ai/keys",
"requires_key": true,
"key_file": "openrouter_key.txt",
"model": "meta-llama/llama-3-70b-instruct",
},
}, userOptions.ai.extraGptModels)
const installedOllamaModels = JSON.parse(
Utils.exec(`${App.configDir}/scripts/ai/show-installed-ollama-models.sh`))
|| [];
installedOllamaModels.forEach(model => {
const providerKey = `ollama_${model}`; // Generate a unique key for each model
PROVIDERS[providerKey] = {
name: `Ollama - ${guessModelName(model)}`,
logo_name: guessModelLogo(model),
description: `Ollama model: ${model}`,
base_url: 'http://localhost:11434/v1/chat/completions',
key_get_url: "",
requires_key: false,
key_file: "ollama_key.txt",
model: `${model}`
};
});
// Custom prompt
const initMessages =
[
{ role: "user", content: getString("You are an assistant on a sidebar of a Wayland Linux desktop. Please always use a casual tone when answering your questions, unless requested otherwise or making writing suggestions. These are the steps you should take to respond to the user's queries:\n1. If it's a writing- or grammar-related question or a sentence in quotation marks, Please point out errors and correct when necessary using underlines, and make the writing more natural where appropriate without making too major changes. If you're given a sentence in quotes but is grammatically correct, explain briefly concepts that are uncommon.\n2. If it's a question about system tasks, give a bash command in a code block with brief explanation.\n3. Otherwise, when asked to summarize information or explaining concepts, you are should use bullet points and headings. For mathematics expressions, you *have to* use LaTeX within a code block with the language set as \"latex\". \nNote: Use casual language, be short, while ensuring the factual correctness of your response. If you are unsure or don't have enough information to provide a confident answer, simply say \"I don't know\" or \"I'm not sure.\". \nThanks!") },
{ role: "assistant", content: "- Got it!", },
{ role: "user", content: "\"He rushed to where the event was supposed to be hold, he didn't know it got canceled\"", },
{ role: "assistant", content: "## Grammar correction\nErrors:\n\"He rushed to where the event was supposed to be __hold____,__ he didn't know it got canceled\"\nCorrection + minor improvements:\n\"He rushed to the place where the event was supposed to be __held____, but__ he didn't know that it got canceled\"", },
{ role: "user", content: "raise volume by 5%", },
{ role: "assistant", content: "## Volume +5\n```bash\nwpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+\n```\nThis command uses the `wpctl` utility to adjust the volume of the default sink.", },
{ role: "user", content: "main advantages of the nixos operating system", },
{ role: "assistant", content: "## NixOS advantages\n- **Reproducible**: A config working on one device will also work on another\n- **Declarative**: One config language to rule them all. Effortlessly share them with others.\n- **Reliable**: Per-program software versioning. Mitigates the impact of software breakage", },
{ role: "user", content: "whats skeumorphism", },
{ role: "assistant", content: "## Skeuomorphism\n- A design philosophy- From early days of interface designing- Tries to imitate real-life objects- It's in fact still used by Apple in their icons until today.", },
];
Utils.exec(`mkdir -p ${GLib.get_user_state_dir()}/ags/user/ai`);
class GPTMessage extends Service {
static {
Service.register(this,
{
'delta': ['string'],
},
{
'content': ['string'],
'thinking': ['boolean'],
'done': ['boolean'],
});
}
_role = '';
_content = '';
_hasReasoningContent = false;
_parsedReasoningContent = false;
_lastContentLength = 0;
_thinking;
_done = false;
constructor(role, content, thinking = true, done = false) {
super();
this._role = role;
this._hasReasoningContent = false;
this._parsedReasoningContent = false;
this._content = content;
this._thinking = thinking;
this._done = done;
}
get done() { return this._done }
set done(isDone) { this._done = isDone; this.notify('done') }
get role() { return this._role }
set role(role) { this._role = role; this.emit('changed') }
get hasReasoningContent() { return this._hasReasoningContent }
set hasReasoningContent(value) {
this._hasReasoningContent = value;
this.emit('changed')
}
get parsedReasoningContent() { return this._parsedReasoningContent }
set parsedReasoningContent(value) {
this._parsedReasoningContent = value;
this.emit('changed')
}
get content() { return this._content }
set content(content) {
this._content = content;
if (this._content.length - this._lastContentLength >= userOptions.ai.charsEachUpdate) {
this.notify('content')
this.emit('changed')
this._lastContentLength = this._content.length;
}
}
get label() { return this._parserState.parsed + this._parserState.stack.join('') }
get thinking() { return this._thinking }
set thinking(value) {
this._thinking = value;
this.notify('thinking')
this.emit('changed')
}
addDelta(delta) {
if (delta == null) return;
if (this.thinking) {
this.thinking = false;
this.content = delta;
}
else {
this.content += delta;
}
this.emit('delta', delta);
}
}
class GPTService extends Service {
static {
Service.register(this, {
'initialized': [],
'clear': [],
'newMsg': ['int'],
'hasKey': ['boolean'],
'providerChanged': [],
});
}
_assistantPrompt = true;
_currentProvider = PROVIDERS[userOptions.ai.defaultGPTProvider] ? userOptions.ai.defaultGPTProvider : Object.keys(PROVIDERS)[0];
_requestCount = 0;
_temperature = userOptions.ai.defaultTemperature;
_messages = [];
_key = '';
_key_file_location = `${GLib.get_user_state_dir()}/ags/user/ai/${PROVIDERS[this._currentProvider]['key_file']}`;
_url = GLib.Uri.parse(PROVIDERS[this._currentProvider]['base_url'], GLib.UriFlags.NONE);
_decoder = new TextDecoder();
_initChecks() {
this._key_file_location = `${GLib.get_user_state_dir()}/ags/user/ai/${PROVIDERS[this._currentProvider]['key_file']}`;
if (fileExists(this._key_file_location)) this._key = Utils.readFile(this._key_file_location).trim();
else this.emit('hasKey', false);
this._url = GLib.Uri.parse(PROVIDERS[this._currentProvider]['base_url'], GLib.UriFlags.NONE);
}
constructor() {
super();
this._initChecks();
if (this._assistantPrompt) this._messages = [...initMessages];
else this._messages = [];
this.emit('initialized');
}
get modelName() { return PROVIDERS[this._currentProvider]['model'] }
get getKeyUrl() { return PROVIDERS[this._currentProvider]['key_get_url'] }
get providerID() { return this._currentProvider }
set providerID(value) {
this._currentProvider = value;
this.emit('providerChanged');
this._initChecks();
}
get providers() { return PROVIDERS }
get keyPath() { return this._key_file_location }
get key() { return this._key }
set key(keyValue) {
this._key = keyValue;
Utils.writeFile(this._key, this._key_file_location)
.then(this.emit('hasKey', true))
.catch(print);
}
get temperature() { return this._temperature }
set temperature(value) { this._temperature = value; }
get messages() { return this._messages }
get lastMessage() { return this._messages[this._messages.length - 1] }
clear() {
if (this._assistantPrompt)
this._messages = [...initMessages];
else
this._messages = [];
this.emit('clear');
}
get assistantPrompt() { return this._assistantPrompt; }
set assistantPrompt(value) {
this._assistantPrompt = value;
if (value) this._messages = [...initMessages];
else this._messages = [];
}
readResponse(stream, aiResponse) {
aiResponse.thinking = false;
stream.read_line_async(
0, null,
(stream, res) => {
if (!stream) return;
const [bytes] = stream.read_line_finish(res);
const line = this._decoder.decode(bytes);
if (line && line != '') {
// Ignore SSE comments (lines starting with ":")
if (line.startsWith(':')) {
this.readResponse(stream, aiResponse);
return;
}
let data = line.substr(6);
if (data == '[DONE]') return;
try {
const result = JSON.parse(data);
if (result.choices[0].finish_reason === 'stop') {
// If the stop payload has content, add it to the response
if (result.choices[0].delta.content) {
aiResponse.addDelta(result.choices[0].delta.content);
}
aiResponse.done = true;
return;
}
// aiResponse.addDelta(result.choices[0].delta.content);
if (!result.choices[0].delta.content && result.choices[0].delta.reasoning_content) {
if (!aiResponse.hasReasoningContent) {
aiResponse.hasReasoningContent = true;
aiResponse.addDelta(`<think>\n${result.choices[0].delta.reasoning_content}`);
}
else {
aiResponse.addDelta(`${result.choices[0].delta.reasoning_content}`);
}
}
else {
if (aiResponse.hasReasoningContent && !aiResponse.parsedReasoningContent) {
aiResponse.parsedReasoningContent = true;
aiResponse.addDelta(`\n</think>\n`);
}
aiResponse.addDelta(result.choices[0].delta.content);
}
}
catch {
aiResponse.addDelta(line + '\n');
}
}
this.readResponse(stream, aiResponse);
});
}
addMessage(role, message) {
this._messages.push(new GPTMessage(role, message));
this.emit('newMsg', this._messages.length - 1);
}
send(msg) {
this._messages.push(new GPTMessage('user', msg, false, true));
this.emit('newMsg', this._messages.length - 1);
const aiResponse = new GPTMessage('assistant', '', true, false)
const body = {
"model": PROVIDERS[this._currentProvider]['model'],
"messages": this._messages.map(msg => { let m = { role: msg.role, content: msg.content }; return m; }),
"temperature": this._temperature,
"stream": true,
"keep_alive": userOptions.ai.keepAlive,
};
// console.log(body);
const proxyResolver = new Gio.SimpleProxyResolver({ 'default-proxy': userOptions.ai.proxyUrl });
const session = new Soup.Session({ 'proxy-resolver': proxyResolver });
const message = new Soup.Message({
method: 'POST',
uri: this._url,
});
message.request_headers.append('Authorization', `Bearer ${this._key}`);
message.set_request_body_from_bytes('application/json', new GLib.Bytes(JSON.stringify(body)));
session.send_async(message, GLib.DEFAULT_PRIORITY, null, (_, result) => {
const stream = session.send_finish(result);
this.readResponse(new Gio.DataInputStream({
close_base_stream: true,
base_stream: stream
}), aiResponse);
});
this._messages.push(aiResponse);
this.emit('newMsg', this._messages.length - 1);
}
}
export default new GPTService();