commit 67b3902bffbe2cad4f4163569e715b24ac50ad86 Author: Ruthenic Date: Thu Oct 13 20:02:18 2022 -0400 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..77c5116 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dist/ +config.json \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c947c79 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "deno.enable": true, + "deno.unstable": true, + "deno.importMap": "import_map.json", + "editor.tabSize": 4, + "editor.detectIndentation": false +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..36d68f2 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# ai_bot +this code should not be used by any sane person, ever. diff --git a/config.template.json b/config.template.json new file mode 100644 index 0000000..2918aac --- /dev/null +++ b/config.template.json @@ -0,0 +1,26 @@ +{ + "general": { + "name": "GPT-4", + "desc": "OpenAI's newest GPT text-generation AI", + "ais": ["huggingface"], + "db": { + "url": "SURREAL_DB_URL_HERE", + "user": "USER_HERE", + "pass": "PASS_HERE", + "namespace": "NS_HERE", + "database": "DB_HERE" + } + }, + "discord": { + "channels": [ + "CHANNEL_ID_HERE" + ] + }, + "huggingface": { + "tokens": [ + "TOKEN_HERE" + ], + "memoryLen": 5, + "model": "EleutherAI/gpt-neo-2.7B" + } +} diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..da198c3 --- /dev/null +++ b/deno.json @@ -0,0 +1,12 @@ +{ + "fmt": { + "options": { + "indentWidth": 4, + "lineWidth": 120 + } + }, + "importMap": "./import_map.json", + "tasks": { + "run:db": "surreal start --user root --pass root rocksdb://dist/database" + } +} diff --git a/import_map.json b/import_map.json new file mode 100644 index 0000000..160f5fe --- /dev/null +++ b/import_map.json @@ -0,0 +1,5 @@ +{ + "imports": { + "@wackford/": "https://deno.land/x/wackford@v0.0.1/" + } +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..e895693 --- /dev/null +++ b/index.ts @@ -0,0 +1 @@ +import "./src/bot.ts"; diff --git a/src/ai/BaseAI.d.ts b/src/ai/BaseAI.d.ts new file mode 100644 index 0000000..995d147 --- /dev/null +++ b/src/ai/BaseAI.d.ts @@ -0,0 +1,20 @@ +import { configType } from "../config.ts"; +import { Channel } from "../database.ts" + +export default class BaseAI { + name: string; + description: string; + history?: string[]; + memory?: string[]; + + constructor(name: string, description: string, config: configType, db: Channel); + + changeShit(opts: { + name?: string; + description?: string; + }): void; + + reset(): void; + + complete(username: string, message: string): Promise; +} diff --git a/src/ai/echo.ts b/src/ai/echo.ts new file mode 100644 index 0000000..92fa6ba --- /dev/null +++ b/src/ai/echo.ts @@ -0,0 +1,28 @@ +class EchoAI { + name: string; + description: string; + + constructor(name: string, description: string, _config: unknown) { + this.name = name; + this.description = description; + } + + reset() { + //NOP + } + + changeShit(opts: { + name?: string; + description?: string; + }) { + this.name = opts.name ?? this.name; + this.description = opts.description ?? this.description; + this.reset(); + } + + complete(_username: string, message: string) { + return message; + } +} + +export default EchoAI; diff --git a/src/ai/huggingface.ts b/src/ai/huggingface.ts new file mode 100644 index 0000000..d9097c9 --- /dev/null +++ b/src/ai/huggingface.ts @@ -0,0 +1,80 @@ +import { configType } from "../config.ts"; +import { Channel } from "../database.ts" + +class HuggingFaceAI { + name: string; + description: string; + prefix: string; + memory: string[]; + tokens: string[]; + tokenNum: number; + memoryLen: number; + parameters: { + max_new_tokens: number; + temperature: number; + repetition_penalty: number; + top_k: number; + return_full_text: boolean; + }; + model: string; + + constructor(name: string, description: string, config: configType, db: Channel) { + this.name = name; + this.description = description; + this.prefix = `The following is a chat with ${this.name}, ${this.description}.\n`; + this.tokens = config.huggingface.tokens; + this.tokenNum = 0; + this.memoryLen = config.huggingface.memoryLen; + this.parameters = { + "max_new_tokens": 50, + "temperature": 0.8, + "repetition_penalty": 1.8, + "top_k": 40, + "return_full_text": false, + }; + this.model = config.huggingface.model; + this.memory = db.history ? db.history.map((m) => `${m.name}: "${m.content}"`) : []; + } + + async #query(prompt: string) { + const res = await fetch(`https://api-inference.huggingface.co/models/${this.model}`, { + body: JSON.stringify({ + inputs: prompt, + parameters: this.parameters, + }), + headers: { + Authorization: `Bearer ${this.tokens[this.tokenNum]}`, + }, + method: "POST", + }); + return await res.json(); + } + + reset() { + this.prefix = `The following is a chat with ${this.name}, ${this.description}.\n`; + this.memory = []; + } + + changeShit(opts: { + name?: string; + description?: string; + }) { + this.name = opts.name ?? this.name; + this.description = opts.description ?? this.description; + this.reset(); + } + + async complete(username: string, message: string) { + console.log(`${username}: "${message}"`); + const ctx = this.memory.slice(this.memoryLen * -2); + const prompt = `${this.prefix}\n${ctx.join("\n")}\n${username}: "${message}"\n${this.name}: "`; + const res = await this.#query(prompt); + const botMsg = res[0].generated_text.split('"\n')[0]; + this.memory.push(`${username}: "${message}"`); + this.memory.push(`${this.name}: "${botMsg}"`); + console.log(`${this.name}: "${botMsg}"`); + return botMsg; + } +} + +export default HuggingFaceAI; diff --git a/src/ai/index.ts b/src/ai/index.ts new file mode 100644 index 0000000..a89989a --- /dev/null +++ b/src/ai/index.ts @@ -0,0 +1,63 @@ +import { Bot, Interaction } from "@wackford/discordeno.ts"; +import { sendInteractionResponse } from "@wackford/mod.ts"; +import { GoofyAhhException } from "../bot.ts"; +import config from "../config.ts"; +import { db, Channel } from "../database.ts"; +import BaseAI from "./BaseAI.d.ts"; +import HuggingFaceAI from "./huggingface.ts"; + +const dirname = new URL(".", import.meta.url).pathname; + +const AIs: Record = new Proxy({} as Record, { + get: (target, prop: string) => { + if (!Object.keys(AIs).includes(prop)) { + return false; + } + return target[prop]; + }, +}); + +export default function initAI() { + config.discord.channels.forEach(async (channelId) => { + let channel = (await db.select(channelId))[0] as Channel; + + if (!channel) { + db.create(channelId, { + moduleName: "huggingface", + name: config.general.name, + description: config.general.desc, + history: [] + }) + + channel = (await db.select(channelId))[0] as Channel; + } + + console.log(channel) + changeAI(channel.moduleName, channelId, channel.name, channel.description) + }); +} + +export async function changeAI(moduleName: string, channelId: string, newName?: string, newDesc?: string) { + const channel = (await db.select(channelId))[0] as Channel; + + const name = newName ?? AIs[channelId].name; + const desc = newDesc ?? AIs[channelId].description; + + const mod = (await import(dirname + moduleName + ".ts")).default; + AIs[channelId] = new mod(name, desc, config, channel); + await db.change(channelId, { moduleName: moduleName }); +} + +export function checkAI(bot: Bot, interaction: Interaction, AI: BaseAI | boolean) { + if (!AI) { + sendInteractionResponse(bot, interaction, { + content: "AI unavailable in this channel; please check your server for the correct channel!", + private: true, + }); + return false; + } else { + return true; + } +} + +export { AIs }; diff --git a/src/bot.ts b/src/bot.ts new file mode 100644 index 0000000..8553d65 --- /dev/null +++ b/src/bot.ts @@ -0,0 +1,26 @@ +import { BotEmitter, initCommands } from "@wackford/mod.ts"; +import { Intents } from "@wackford/discordeno.ts"; +import initLocalCommands from "./commands/index.ts"; +import initOnMessage from "./events/onMessage.ts"; +import initAIs from "./ai/index.ts"; +import initDB from "./database.ts"; + +export class GoofyAhhException extends Error { + constructor(message: string) { + super(message); + this.name = "GoofyAhhException"; + } +} + +await initDB(); +initOnMessage(); +initLocalCommands(); +initCommands(); +initAIs(); + +await BotEmitter.emit("start", { + token: Deno.env.get("TOKEN") ?? (() => { + throw new GoofyAhhException("No token?"); + })(), + intents: Intents.Guilds | Intents.GuildMessages | Intents.MessageContent, +}); diff --git a/src/commands/change.ts b/src/commands/change.ts new file mode 100644 index 0000000..36105c6 --- /dev/null +++ b/src/commands/change.ts @@ -0,0 +1,44 @@ +import { sendInteractionResponse, SlashCommandOptions } from "@wackford/mod.ts"; +import { ApplicationCommandOptionTypes } from "@wackford/discordeno.ts"; +import { AIs, checkAI } from "../ai/index.ts"; +import { db } from "../database.ts"; + +export default { + name: "change", + description: "shows the current name and description of the chatbot", + options: [ + { + name: "name", + description: "sets the name of the bot to this value", + type: ApplicationCommandOptionTypes.String, + }, + { + name: "description", + description: "sets the description of the bot to this value", + type: ApplicationCommandOptionTypes.String, + }, + ], + execute: async (bot, interaction, args) => { + const channelId = interaction?.channelId?.toString() as string; + const AI = AIs[channelId]; + const newName = args?.name?.value?.toString() ?? undefined; + const newDesc = args?.description?.value?.toString() ?? undefined; + + if (!checkAI(bot, interaction, AI)) return; + + AI.changeShit({ + name: newName, + description: newDesc, + }); + + db.change(channelId, { + name: newName, + description: newDesc, + history: [] + }) + + await sendInteractionResponse(bot, interaction, { + content: `Name: ${newName ?? "[unchanged]"}\nDescription: ${newDesc ?? "[unchanged]"}`, + }); + }, +} as SlashCommandOptions; diff --git a/src/commands/changeAI.ts b/src/commands/changeAI.ts new file mode 100644 index 0000000..cc430d6 --- /dev/null +++ b/src/commands/changeAI.ts @@ -0,0 +1,40 @@ +import { ApplicationCommandOptionChoice, ApplicationCommandOptionTypes } from "@wackford/discordeno.ts"; +import { sendInteractionResponse, SlashCommandOptions } from "@wackford/mod.ts"; +import { AIs, changeAI, checkAI } from "../ai/index.ts"; +import config from "../config.ts"; +import { db } from "../database.ts"; + +export default { + name: "changeai", + description: "changes the AI", + options: [ + { + name: "name", + description: "name of the", + type: ApplicationCommandOptionTypes.String, + required: true, + choices: config.general.ais.map((m) => + { + name: m, + value: m, + } + ), + }, + ], + execute: async (bot, interaction, args) => { + const AI = AIs[interaction?.channelId?.toString() as string]; + const name = args["name"].value?.toString(); + + if (!checkAI(bot, interaction, AI)) return; + + if (name) { + await changeAI(name, interaction.channelId?.toString() as string); + db.change(interaction?.channelId?.toString() as string, { + history: [] + }) + } + await sendInteractionResponse(bot, interaction, { + content: `Changed AI to ${name}!`, + }); + }, +} as SlashCommandOptions; diff --git a/src/commands/debug.ts b/src/commands/debug.ts new file mode 100644 index 0000000..f75a2ba --- /dev/null +++ b/src/commands/debug.ts @@ -0,0 +1,17 @@ +import { sendInteractionResponse, SlashCommandOptions } from "@wackford/mod.ts"; +import { AIs, checkAI } from "../ai/index.ts"; + +export default { + name: "debug", + description: "shows debug information", + execute: async (bot, interaction) => { + const AI = AIs[interaction?.channelId?.toString() as string]; + + if (!checkAI(bot, interaction, AI)) return; + + await sendInteractionResponse(bot, interaction, { + content: `Name: ${AI.name}\nDescription: ${AI.description}\nAI: ${AI.constructor.name}\nHistory: ${JSON.stringify(AI.memory ?? AI.history ?? "[unused]")}`, + private: true + }); + }, +} as SlashCommandOptions; diff --git a/src/commands/index.ts b/src/commands/index.ts new file mode 100644 index 0000000..28b5533 --- /dev/null +++ b/src/commands/index.ts @@ -0,0 +1,27 @@ +import { createSlashCommand, sendInteractionResponse } from "@wackford/mod.ts"; + +const dirname = new URL(".", import.meta.url).pathname; + +const helpMessage: string[] = [ + "/help - helps your idiot ass", // we do this manually because fuck you too +]; + +export default async function initLocalCommands() { + for await (const file of Deno.readDir(dirname)) { + if (file.name !== "index.ts") { + const mod = (await import(dirname + file.name)).default; + helpMessage.push(`/${mod.name} - ${mod.description}`); + createSlashCommand(mod); + } + } + createSlashCommand({ + name: "help", + description: "helps your idiot ass", + execute: async (bot, interaction) => { + await sendInteractionResponse(bot, interaction, { + content: helpMessage.join("\n"), + private: true, + }); + }, + }); +} diff --git a/src/commands/reset.ts b/src/commands/reset.ts new file mode 100644 index 0000000..79c8464 --- /dev/null +++ b/src/commands/reset.ts @@ -0,0 +1,24 @@ +import { sendInteractionResponse, SlashCommandOptions } from "@wackford/mod.ts"; +import { AIs, checkAI } from "../ai/index.ts"; +import { db } from "../database.ts"; + +export default { + name: "reset", + description: "resets the conversation history of the chatbot", + execute: async (bot, interaction) => { + const channelId = interaction?.channelId?.toString() as string; + const AI = AIs[interaction?.channelId?.toString() as string]; + + if (!checkAI(bot, interaction, AI)) return; + + AI.reset(); + + db.change(channelId, { + history: [] + }) + + await sendInteractionResponse(bot, interaction, { + content: `Done!`, + }); + }, +} as SlashCommandOptions; diff --git a/src/commands/status.ts b/src/commands/status.ts new file mode 100644 index 0000000..b6233db --- /dev/null +++ b/src/commands/status.ts @@ -0,0 +1,16 @@ +import { sendInteractionResponse, SlashCommandOptions } from "@wackford/mod.ts"; +import { AIs, checkAI } from "../ai/index.ts"; + +export default { + name: "status", + description: "shows the current name and description of the chatbot", + execute: async (bot, interaction) => { + const AI = AIs[interaction?.channelId?.toString() as string]; + + if (!checkAI(bot, interaction, AI)) return; + + await sendInteractionResponse(bot, interaction, { + content: `Name: ${AI.name}\nDescription: ${AI.description}\nAI: ${AI.constructor.name}`, + }); + }, +} as SlashCommandOptions; diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..610640b --- /dev/null +++ b/src/config.ts @@ -0,0 +1,7 @@ +import config from "../config.json" assert { type: "json" }; + +export type configType = typeof config; + +export { config }; + +export default config; diff --git a/src/database.ts b/src/database.ts new file mode 100644 index 0000000..0d9913d --- /dev/null +++ b/src/database.ts @@ -0,0 +1,28 @@ +import Surreal from "https://deno.land/x/surrealdb@v0.5.0/mod.ts"; +import config from "./config.ts"; +import { GoofyAhhException } from "./bot.ts"; + +export const db = new Surreal(`${config.general.db.url}/rpc`); + +export interface Channel { + moduleName: string; + name: string; + description: string; + history: { + name: string, + content: string + }[]; +} + +export default async function init() { + try { + await db.signin({ + user: config.general.db.user, + pass: config.general.db.pass + }); + + await db.use(config.general.db.namespace, config.general.db.database); + } catch (e) { + throw new GoofyAhhException(e); + } +} diff --git a/src/events/onMessage.ts b/src/events/onMessage.ts new file mode 100644 index 0000000..bdd7004 --- /dev/null +++ b/src/events/onMessage.ts @@ -0,0 +1,49 @@ +import { db, Channel } from "../database.ts"; +import { BotEmitter } from "@wackford/mod.ts"; +import { AIs } from "../ai/index.ts"; +import config from "../../config.json" assert { type: "json" }; + +//const dirname = new URL(".", import.meta.url).pathname; + +export default function init() { + BotEmitter.on("message", async (bot, message) => { + if ( + message.content && + !message.content.startsWith("!") && + config.discord.channels.includes(message.channelId.toString()) && + !message.isFromBot && + message.member + ) { + const channel = (await db.select(message.channelId.toString()))[0] as Channel + const user = await bot.helpers.getUser(message.member?.id); + + await bot.helpers.startTyping(message.channelId); + + const res = await AIs[message.channelId.toString()].complete(user.username, message.content) + + const history = channel.history ?? [] + + history.push({ + name: user.username, + content: message.content + }, { + name: AIs[message.channelId.toString()].name, + content: res + }) + + db.change(message.channelId.toString(), { + history + }) + + await bot.helpers.sendMessage(message.channelId, { + content: `[${AIs[message.channelId.toString()].name}] ${res}`, + messageReference: { + messageId: message.id, + channelId: message.channelId, + guildId: message.guildId, + failIfNotExists: false, + }, + }); + } + }); +}