diff --git a/.env.example b/.env.example index 825067c..d4ccc7b 100644 --- a/.env.example +++ b/.env.example @@ -34,3 +34,4 @@ ALLOWED_CHAT= SAFE_WORD= # When this character/string is detected anywhere in a message, the bot won't respond to it. Defaults to "\". + diff --git a/src/bot.ts b/src/bot.ts index 5557812..0097818 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,7 +1,7 @@ -import { TailchatWsClient, stripMentionTag } from "tailchat-client-sdk"; +import { TailchatWsClient } from "tailchat-client-sdk"; import { OpenAI } from "openai"; import * as fs from "fs"; -import { ImageRequestData, ImageSize, Messages } from "./types"; +import { GuildData, ImageRequestData, ImageSize } from "./types"; import { checkFile, getUsername, @@ -20,7 +20,6 @@ import { createImageModel, temperature, key, - system, } from "./assistant"; import { ImagesResponse } from "openai/resources"; import chalk from "chalk"; @@ -28,9 +27,9 @@ import dotenv from "dotenv"; dotenv.config(); // Specific to Tailchat. The endpoint of my Tailchat server, the bot ID and Secret. -const HOST = process.env.HOST; -const APPID = process.env.ID; -const APPSECRET = process.env.SECRET; +export const HOST = process.env.HOST; +export const APPID = process.env.ID; +export const APPSECRET = process.env.SECRET; const allVarsFilled = HOST && APPID && APPSECRET; if (!allVarsFilled) { @@ -39,8 +38,13 @@ if (!allVarsFilled) { } // Define the initial system message for the LLM. -const session: Messages = checkFile("./messages.json", "utf-8", system.normal); -console.log("Our conversation is:", session); +const session = new GuildData(checkFile("./messages.json", "utf-8")); + +session.data.toString() + ? console.log("Our conversation is:", session.data) + : console.log( + "Looks like we're starting fresh, no previous chat history was found.", + ); const THINKING = "[md]`Thinking...`[/md]"; @@ -117,10 +121,13 @@ client.connect().then(async () => { content: "[md]`Analyzing image...`[/md]", }); - session.push(formatImageMessage(username, imageData)); + session.appendMessage( + formatImageMessage(username, imageData), + message.converseId, + ); const response = await assistant.chat.completions.create({ - messages: session, + messages: session.getHistory(message.converseId), model: imageModel, temperature: temperature, }); @@ -129,7 +136,7 @@ client.connect().then(async () => { "", ); - session.push(messageOf(response)); + session.appendMessage(messageOf(response), message.converseId); await client.sendMessage({ converseId: message.converseId, @@ -138,7 +145,11 @@ client.connect().then(async () => { content: `[md]${contentOf(response)}[/md]`, }); - fs.writeFileSync("./messages.json", JSON.stringify(session), "utf8"); + fs.writeFileSync( + "./messages.json", + JSON.stringify(session.data), + "utf8", + ); console.log("Now our conversation is", session); } else { const username = await getUsername(HOST, message.author!); @@ -149,15 +160,18 @@ client.connect().then(async () => { content: THINKING, }); - session.push(formatUserMessage(username, message.content)); + session.appendMessage( + formatUserMessage(username, message.content), + message.converseId, + ); const response = await assistant.chat.completions.create({ - messages: session, + messages: session.getHistory(message.converseId), model: textModel, temperature: temperature, }); - session.push(messageOf(response)); + session.appendMessage(messageOf(response), message.converseId); await client.sendMessage({ converseId: message.converseId, @@ -166,7 +180,11 @@ client.connect().then(async () => { content: `[md]${contentOf(response)}[/md]`, }); - fs.writeFileSync("./messages.json", JSON.stringify(session), "utf8"); + fs.writeFileSync( + "./messages.json", + JSON.stringify(session.data), + "utf8", + ); } } catch (err) { console.log("Failed", err); @@ -187,22 +205,29 @@ client.connect().then(async () => { content: THINKING, }); - session.push(formatUserMessage(username, message.content)); + session.appendMessage( + formatUserMessage(username, message.content), + message.converseId, + ); const response = await assistant.chat.completions.create({ - messages: session, + messages: session.getHistory(message.converseId), model: textModel, temperature: temperature, }); - session.push(messageOf(response)); + session.appendMessage(messageOf(response), message.converseId); await client.sendMessage({ converseId: message.converseId, content: `[md]${contentOf(response)}[/md]`, }); - fs.writeFileSync("./messages.json", JSON.stringify(session), "utf8"); + fs.writeFileSync( + "./messages.json", + JSON.stringify(session.data), + "utf8", + ); } catch (err) { console.log("Failed", err); diff --git a/src/types.ts b/src/types.ts index 6b15ac3..c5ac7f5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,6 +5,10 @@ import { ChatCompletionSystemMessageParam, } from "openai/resources"; import { TailchatWsClient } from "tailchat-client-sdk"; +import { ChatMessage } from "tailchat-types"; +import { formatNewHistory, formatUserMessage, getUsername } from "./utils"; +import { HOST } from "./bot"; +import chalk from "chalk"; export interface ImageMessage { role: "user"; @@ -27,6 +31,8 @@ export type AnyChatCompletion = | ChatCompletionUserMessageParam | ChatCompletionSystemMessageParam; +export type AnyChatCompletionRole = "user" | "system" | "assistant"; + export enum ImageSize { Small = "256x256", Medium = "512x512", @@ -82,4 +88,45 @@ export type Temperature = | 1.9 | 2.0; -export type Messages = Array; +export type Messages = (AnyChatCompletion | ImageMessage)[]; + +export interface ChatHistoryData { + id: string; + history: Messages; +} + +export type ChatHistory = ChatHistoryData[]; + +export class GuildData { + data: ChatHistory; + + constructor(existingData?: ChatHistory) { + this.data = existingData || []; + } + + appendMessage( + message: AnyChatCompletion | ImageMessage, + converseId: string, + ): void { + for (const converse of this.data) { + if (converseId === converse.id) { + converse.history.push(message); + return; + } + } + + this.data.push(formatNewHistory(message, converseId)); + } + + getHistory(converseId: string): Messages { + for (const converse of this.data) { + if (converseId === converse.id) { + return converse.history; + } + } + + throw new Error( + `No history was found with the given converse id: ${chalk.green(converseId)}. Something isn't right.`, + ); + } +} diff --git a/src/utils.ts b/src/utils.ts index 48acd4b..e98d3b9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,28 +1,70 @@ import * as fs from "fs"; -import { AnyChatCompletion, ImageMessage, Messages } from "./types"; +import { + AnyChatCompletion, + AnyChatCompletionRole, + ChatHistory, + ChatHistoryData, + ImageMessage, +} from "./types"; +import { system } from "./assistant"; import { ChatCompletion, ChatCompletionMessage } from "openai/resources"; import { stripMentionTag } from "tailchat-client-sdk"; +import { ChatMessage } from "tailchat-types"; +import { HOST } from "./bot"; +import chalk from "chalk"; + +//export function checkFile( +// file: string, +// encoding: fs.EncodingOption, +// defaultContent: string, +//): Messages { +// let final: Messages; +// const generic: Messages = [ +// { +// role: "system", +// content: defaultContent, +// }, +// ]; +// +// if (fs.existsSync(file)) { +// const data = fs.readFileSync(file, encoding); +// +// final = !data.toString().trim() ? generic : JSON.parse(data.toString()); +// } else { +// fs.createWriteStream(file); +// final = generic; +// } +// +// return final; +//} export function checkFile( file: string, encoding: fs.EncodingOption, - defaultContent: string, -): Messages { - let final: Messages; - const generic: Messages = [ - { - role: "system", - content: defaultContent, - }, - ]; +): ChatHistory { + let final: ChatHistory; if (fs.existsSync(file)) { const data = fs.readFileSync(file, encoding); - final = !data.toString().trim() ? generic : JSON.parse(data.toString()); + final = !data.toString().trim() ? [] : JSON.parse(data.toString()); + + try { + // @ts-ignore + if (final.at(0).role) { + console.warn( + chalk.yellow( + "Your persistent storage uses the old data structure for persistent messages. These messages will be moved to a backup file and the existing file will be overwritten.", + ), + ); + fs.writeFileSync(`${file}.bak`, data); + + final = []; + } + } catch {} } else { fs.createWriteStream(file); - final = generic; + final = []; } return final; @@ -59,6 +101,23 @@ export function formatImageMessage( }; } +export function formatNewHistory( + message: AnyChatCompletion | ImageMessage, + converseId: string, + customSystemMessage?: string, +): ChatHistoryData { + return { + id: converseId, + history: [ + { + role: "system", + content: customSystemMessage || system.normal, + }, + message, + ], + }; +} + export function messageOf(response: ChatCompletion): ChatCompletionMessage { return response.choices.at(0)!.message; }