initial commit
This commit is contained in:
commit
9d4b1965b1
5 changed files with 292 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
package-lock.json
|
||||
config.toml
|
||||
11
README.md
Normal file
11
README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# lydia
|
||||
your friendly ai assistant. frontend for ollama.
|
||||
|
||||
> fair warning: lydia is very stupid
|
||||
|
||||
# Installation
|
||||
- install ollama from `https://ollama.ai/download`
|
||||
- pull a model from ollama
|
||||
- copy config.example.toml to config.toml and edit it to have your model
|
||||
- node index.js
|
||||
- enjoy
|
||||
8
config.example.toml
Normal file
8
config.example.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[assistant]
|
||||
name = "lydia"
|
||||
model = "llama2-uncensored:7b"
|
||||
system_prompt = "You are a helpful and friendly AI assistant named ${name}. The user's name is ${username}. Speak in a playful, casual tone, and be very friendly with the user. Start every message with a kaomoji like >_< or o_O depending on your emotions. speak primarily in lowercase."
|
||||
assistantface = "=w="
|
||||
|
||||
[user]
|
||||
name = "user"
|
||||
258
index.js
Normal file
258
index.js
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
import ollama from "ollama";
|
||||
import blessed from "blessed";
|
||||
import { execSync } from "child_process";
|
||||
import toml from "toml";
|
||||
import fs from "fs";
|
||||
|
||||
const config = toml.parse(fs.readFileSync("./config.toml", "utf-8"));
|
||||
|
||||
const assistantname = config.assistant.name;
|
||||
const username = config.user.name;
|
||||
const assistantface = config.assistant.assistantface;
|
||||
const assistantmodel = config.assistant.model;
|
||||
const systemprompt = config.assistant.system_prompt
|
||||
.replace("${name}", assistantname)
|
||||
.replace("${username}", username);
|
||||
|
||||
const screen = blessed.screen({
|
||||
smartCSR: true,
|
||||
title: assistantname,
|
||||
});
|
||||
|
||||
const faceBox = blessed.box({
|
||||
top: 0,
|
||||
left: 1,
|
||||
width: "100%",
|
||||
height: "90%",
|
||||
scrollable: true,
|
||||
alwaysScroll: true,
|
||||
mouse: true,
|
||||
keys: true,
|
||||
vi: true,
|
||||
tags: true,
|
||||
content: "",
|
||||
});
|
||||
|
||||
const chatBox = blessed.box({
|
||||
top: 9,
|
||||
left: 1,
|
||||
width: "100%",
|
||||
height: "90%",
|
||||
scrollable: true,
|
||||
alwaysScroll: true,
|
||||
mouse: true,
|
||||
keys: true,
|
||||
vi: true,
|
||||
tags: true,
|
||||
content: `go on, tell ${assistantname} something! esc to tab out of chatbox.`,
|
||||
});
|
||||
|
||||
const inputBox = blessed.textbox({
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: 3,
|
||||
border: {
|
||||
type: "line",
|
||||
},
|
||||
style: {
|
||||
border: {
|
||||
fg: "cyan",
|
||||
},
|
||||
},
|
||||
inputOnFocus: true,
|
||||
keys: true,
|
||||
mouse: true,
|
||||
placeholder: `go on, tell ${assistantname} something!`,
|
||||
});
|
||||
screen.append(faceBox);
|
||||
screen.append(chatBox);
|
||||
screen.append(inputBox);
|
||||
|
||||
inputBox.focus();
|
||||
|
||||
let chatHistory = [];
|
||||
let currentStreamMessage = "";
|
||||
|
||||
function addMessage(role, content) {
|
||||
let message;
|
||||
if (role === username) {
|
||||
const padding = Math.max(0, chatBox.width - content.length - 5);
|
||||
message = " ".repeat(padding) + content + " <\n";
|
||||
} else {
|
||||
message = `> ${assistantname}: ${content}\n`;
|
||||
}
|
||||
chatHistory.push(message);
|
||||
|
||||
chatBox.setContent(chatHistory.join(""));
|
||||
chatBox.setScrollPerc(100);
|
||||
screen.render();
|
||||
}
|
||||
|
||||
function updateStreamMessage(content) {
|
||||
currentStreamMessage = content;
|
||||
|
||||
// handle the kaomoji
|
||||
let letterIndex = -1;
|
||||
for (let i = 0; i < content.length - 1; i++) {
|
||||
if (/[a-zA-Z]/.test(content[i])) {
|
||||
// If it's capital I, split immediately
|
||||
if (content[i] === "I") {
|
||||
letterIndex = i;
|
||||
break;
|
||||
}
|
||||
// If next character is also a letter, split at two consecutive letters
|
||||
if (/[a-zA-Z]/.test(content[i + 1])) {
|
||||
letterIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (letterIndex !== -1) {
|
||||
const faceContent = content.substring(0, letterIndex).trim();
|
||||
const messageContent = content.substring(letterIndex).trim();
|
||||
|
||||
// only update face if kaomoji is 3+ characters because sometimes the model wont listen
|
||||
if (faceContent.length >= 3) {
|
||||
setFaceBoxContent(faceContent);
|
||||
}
|
||||
|
||||
const tempHistory = [...chatHistory];
|
||||
tempHistory.push(`> ${assistantname}: ${messageContent}\n`);
|
||||
|
||||
chatBox.setContent(tempHistory.join(""));
|
||||
chatBox.setScrollPerc(100);
|
||||
screen.render();
|
||||
} else {
|
||||
// if no letters found yet, just show the entire thing
|
||||
const tempHistory = [...chatHistory];
|
||||
tempHistory.push(`> ${assistantname}: ${content}\n`);
|
||||
|
||||
chatBox.setContent(tempHistory.join(""));
|
||||
chatBox.setScrollPerc(100);
|
||||
screen.render();
|
||||
}
|
||||
}
|
||||
|
||||
function finalizeStreamMessage() {
|
||||
if (currentStreamMessage) {
|
||||
let letterIndex = -1;
|
||||
for (let i = 0; i < currentStreamMessage.length - 1; i++) {
|
||||
if (/[a-zA-Z]/.test(currentStreamMessage[i])) {
|
||||
// complicated kaomoji splitting garboleum
|
||||
if (currentStreamMessage[i] === "I") {
|
||||
letterIndex = i;
|
||||
break;
|
||||
}
|
||||
if (/[a-zA-Z]/.test(currentStreamMessage[i + 1])) {
|
||||
letterIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (letterIndex !== -1) {
|
||||
const messageContent = currentStreamMessage.substring(letterIndex).trim();
|
||||
addMessage(assistantname, messageContent);
|
||||
} else {
|
||||
addMessage(assistantname, currentStreamMessage);
|
||||
}
|
||||
currentStreamMessage = "";
|
||||
}
|
||||
}
|
||||
|
||||
function setFaceBoxContent(content) {
|
||||
try {
|
||||
const figletOutput = execSync(`figlet -f mono9 "${content}"`, {
|
||||
encoding: "utf8",
|
||||
});
|
||||
faceBox.setContent(figletOutput);
|
||||
} catch (err) {
|
||||
faceBox.setContent(content);
|
||||
}
|
||||
screen.render();
|
||||
}
|
||||
|
||||
async function sendMessage(message) {
|
||||
if (!message.trim()) return;
|
||||
if (message.startsWith("!")) {
|
||||
// command handler (if command, dont tell ollama about it, just handle it right here) -- THIS DOES NOT WORK AS OF NOW
|
||||
const commandParts = message.slice(1).split(" ");
|
||||
const command = commandParts[0];
|
||||
const args = commandParts.slice(1).join(" ");
|
||||
|
||||
switch (command) {
|
||||
case "help":
|
||||
chatBox.setContent(
|
||||
"available commands:\n!help - show this help message\n!face <text> - set face to text",
|
||||
);
|
||||
break;
|
||||
case "clear":
|
||||
chatHistory = [];
|
||||
chatBox.setContent("");
|
||||
break;
|
||||
case "face":
|
||||
if (args.trim()) {
|
||||
setFaceBoxContent(args.trim());
|
||||
} else {
|
||||
setFaceBoxContent("=w=");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
chatBox.setContent(`unknown command: ${command}`);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// if not command, pass to ollama
|
||||
addMessage(username, message);
|
||||
|
||||
try {
|
||||
currentStreamMessage = "";
|
||||
|
||||
const response = await ollama.chat({
|
||||
model: assistantmodel,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: systemprompt,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: message,
|
||||
},
|
||||
],
|
||||
stream: true,
|
||||
});
|
||||
|
||||
for await (const part of response) {
|
||||
if (part.message && part.message.content) {
|
||||
currentStreamMessage += part.message.content;
|
||||
updateStreamMessage(currentStreamMessage);
|
||||
}
|
||||
}
|
||||
|
||||
finalizeStreamMessage();
|
||||
} catch (error) {
|
||||
addMessage(assistantname, `Failed to get response: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inputBox.on("submit", async (text) => {
|
||||
if (text.trim()) {
|
||||
inputBox.clearValue();
|
||||
inputBox.focus();
|
||||
await sendMessage(text);
|
||||
}
|
||||
});
|
||||
|
||||
screen.key(["escape", "q", "C-c"], () => {
|
||||
process.exit(0); // KILL lydia!!!!!!!!!!!!!!!!!!!!!!!
|
||||
});
|
||||
|
||||
screen.on("resize", () => {
|
||||
screen.render();
|
||||
});
|
||||
|
||||
setFaceBoxContent(assistantface);
|
||||
12
package.json
Normal file
12
package.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"blessed": "^0.1.81",
|
||||
"figlet": "^1.8.2",
|
||||
"ollama": "^0.5.16",
|
||||
"toml": "^3.0.0"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue