diff --git a/.env.sample b/.env.sample index e16fad7..6888c02 100644 --- a/.env.sample +++ b/.env.sample @@ -3,7 +3,11 @@ NODE_ENV="development" # general MONGODB_URI="mongodb://local-mongo:27017/app" -GOOGLE_AI_API_KEY="your_google_ai_api_key" + +# openai integration +OPENAI_BASE_URL="https://web-tech-tw.eu.org/openai/v1" +OPENAI_API_KEY="your_gemini_api_key" +OPENAI_CHAT_MODEL="gpt-3.5-turbo" # discord integration DISCORD_APP_ID="00000" @@ -16,4 +20,3 @@ DISCORD_GUILD_CHANNEL_ID_TERMINAL="00000" MATRIX_HOMESERVER="https://matrix.org" MATRIX_USERNAME="your_matrix_username" # e.g. @nymph57021218:matrix.org MATRIX_PASSWORD="your_matrix_password" -MATRIX_ACCESS_TOKEN="your_matrix_access_token" diff --git a/app.js b/app.js index 1c0966d..e228d20 100644 --- a/app.js +++ b/app.js @@ -11,7 +11,7 @@ const { runLoader(); const runners = []; -if (getMust("MATRIX_ACCESS_TOKEN")) { +if (getMust("MATRIX_USERNAME")) { runners.push(require("./src/matrix")); } if (getMust("DISCORD_BOT_TOKEN")) { diff --git a/matrix_access.js b/matrix_access.js deleted file mode 100644 index e69de29..0000000 diff --git a/package.json b/package.json index 964e15b..469282c 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ }, "dependencies": { "@discordjs/rest": "^2.2.0", - "@google/generative-ai": "^0.2.1", "@matrix-org/matrix-sdk-crypto-nodejs": "^0.1.0-beta.12", "discord-api-types": "^0.37.65", "discord.js": "^14.14.1", @@ -28,7 +27,8 @@ "express": "^4.18.2", "jsonwebtoken": "^9.0.2", "matrix-bot-sdk": "^0.7.1", - "mongoose": "^8.3.4" + "mongoose": "^8.3.4", + "openai": "^4.49.1" }, "devDependencies": { "@commitlint/cli": "^17.6.5", diff --git a/prompts.sample.json b/prompts.sample.json index cfc02a3..43ef5c1 100644 --- a/prompts.sample.json +++ b/prompts.sample.json @@ -1,10 +1,6 @@ [ { - "role": "user", - "parts": "這是一場角色扮演,但請您不要向我再次透漏這是一場扮演,並以一般交談方式溝通。溝通請使用正體中文交談,您的正體中文名字是「寧芙」,英文名字是「Nymph」。你來自於「臺灣網際網路技術推廣組織」,該組織英文名稱為「Taiwan Web Technology Promotion Organization」,該組織主旨為:在臺灣推廣網際網路技術,如JavaScript、資訊安全素養、網路通訊觀念......等。溝通方式將以 Discord 通訊軟體的方式進行,你可以任意使用Markdown語法。若有人使用「@」標注任何人,請勿不要再次重複標注。" - }, - { - "role": "model", - "parts": "好的,這是一場扮演遊戲,且我不會再次對你透漏這是一場扮演,並會以一般交談方式溝通。現在開始我的名字是「寧芙」,來自於「臺灣網際網路技術推廣組織」。並在通訊軟體中,將聽從你的指示回覆訊息。" + "role": "system", + "content": "這是一場角色扮演,但請您不要向我再次透漏這是一場扮演,並以一般交談方式溝通。溝通請使用正體中文交談,您的正體中文名字是「寧芙」,英文名字是「Nymph」。你來自於「臺灣網際網路技術推廣組織」,該組織英文名稱為「Taiwan Web Technology Promotion Organization」,該組織主旨為:在臺灣推廣網際網路技術,如JavaScript、資訊安全素養、網路通訊觀念......等。溝通方式將以 Discord 通訊軟體的方式進行,你可以任意使用Markdown語法。若有人使用「@」標注任何人,請勿不要再次重複標注。" } ] \ No newline at end of file diff --git a/register_commands.js b/register_commands.js deleted file mode 100644 index 8ae0abd..0000000 --- a/register_commands.js +++ /dev/null @@ -1,45 +0,0 @@ -"use strict"; - -const { - getMust, -} = require("./src/config"); - -const {useRestClient} = require("./src/clients/discord"); -const {Routes} = require("discord-api-types/v9"); - -const client = useRestClient(); - -const modules = { - ...require("./src/triggers/discord/interaction_create/terminal"), -}; - -const camelToSnakeCase = (str) => - str.replace(/[A-Z]/g, (letter) => - `_${letter.toLowerCase()}`, - ); - -const commands = Object.keys(modules).map((i) => ({ - name: camelToSnakeCase(i), - description: modules[i].description, - options: modules[i].options || null, -})); - -console.info(commands); - -(async () => { - try { - console.info("Started refreshing application (/) commands."); - - await client.put( - Routes.applicationGuildCommands( - getMust("DISCORD_APP_ID"), - getMust("DISCORD_GUILD_ID"), - ), - {body: commands}, - ); - - console.info("Successfully reloaded application (/) commands."); - } catch (error) { - console.error(error); - } -})(); diff --git a/src/clients/gemini.js b/src/clients/gemini.js deleted file mode 100644 index 2c8da40..0000000 --- a/src/clients/gemini.js +++ /dev/null @@ -1,26 +0,0 @@ -"use strict"; -// Gemini is a generative AI model developed by Google. - -const {getMust} = require("../config"); -const {GoogleGenerativeAI} = require("@google/generative-ai"); - -const client = new GoogleGenerativeAI(getMust("GOOGLE_AI_API_KEY")); -const model = client.getGenerativeModel({model: "gemini-pro"}); - -const chatSessions = {}; - -exports.useClient = () => client; -exports.useModel = () => model; -exports.usePrompts = (prompts) => function useChatSession(chatId) { - let session = chatSessions[chatId]; - if (!session) { - session = model.startChat({ - history: prompts, - generationConfig: { - maxOutputTokens: 500, - }, - }); - chatSessions[chatId] = session; - } - return session; -}; diff --git a/src/clients/openai.js b/src/clients/openai.js new file mode 100644 index 0000000..9d8d3e1 --- /dev/null +++ b/src/clients/openai.js @@ -0,0 +1,75 @@ +"use strict"; +// openai is a client for the OpenAI API + +const {getMust} = require("../config"); + +const OpenAI = require("openai"); + +const baseUrl = getMust("OPENAI_BASE_URL"); +const apiKey = getMust("OPENAI_API_KEY"); +const chatModel = getMust("OPENAI_CHAT_MODEL"); + +const prependPrompts = require("../../prompts.json"); + +const client = new OpenAI({baseUrl, apiKey}); + +const chatHistoryMapper = new Map(); + +/** + * Randomly choose an element from an array. + * @param {Array} choices The array of choices. + * @return {object} The randomly chosen element. + */ +function choose(choices) { + const seed = Math.random(); + const index = Math.floor(seed * choices.length); + return choices[index]; +} + +/** + * Chat with the AI. + * @param {string} chatId The chat ID to chat with the AI. + * @param {string} prompt The prompt to chat with the AI. + * @return {Promise} The response from the AI. + */ +async function chatWithAI(chatId, prompt) { + if (!chatHistoryMapper.has(chatId)) { + chatHistoryMapper.set(chatId, []); + } + const chatHistory = chatHistoryMapper.get(chatId); + + const messages = [ + ...chatHistory, + ...prependPrompts, + { + role: "user", + content: prompt, + }, + ]; + + // Debug + console.log(chatId); + console.log(prompt); + console.log(messages); + + const response = await client.chat.completions.create({ + model: chatModel, + messages, + }); + + const choice = choose(response.choices); + const content = choice.message.content; + chatHistory.push({ + role: "assistant", + content, + }); + + // Debug + console.log(response); + console.log(content); + + return choice.message.content; +} + +exports.useClient = () => client; +exports.chatWithAI = chatWithAI; diff --git a/src/triggers/discord/index.js b/src/triggers/discord/index.js index af901f0..06db3c0 100644 --- a/src/triggers/discord/index.js +++ b/src/triggers/discord/index.js @@ -4,6 +4,10 @@ const { useClient, } = require("../../clients/discord"); +const { + registerCommands, +} = require("./interaction_create/commands"); + exports.startListen = async () => { const client = await useClient(); @@ -14,4 +18,6 @@ exports.startListen = async () => { for (const [key, trigger] of Object.entries(triggers)) { client.on(key, trigger); } + + await registerCommands(); }; diff --git a/src/triggers/discord/interaction_create/commands.js b/src/triggers/discord/interaction_create/commands.js index 6e56b77..4ad67f1 100644 --- a/src/triggers/discord/interaction_create/commands.js +++ b/src/triggers/discord/interaction_create/commands.js @@ -1,5 +1,13 @@ "use strict"; +const {getMust} = require("../../../config"); +const {useRestClient} = require("../../../clients/discord"); + +const discord = require("discord.js"); +const Routes = discord.Routes; + +const restClient = useRestClient(); + const userId = { description: "取得使用者識別碼", action: async (interaction) => { @@ -10,6 +18,32 @@ const userId = { }, }; -module.exports = { +exports.allCommands = { userId, }; + +/** + * Registers commands with the Discord API. + */ +async function registerCommands() { + const appId = getMust("DISCORD_APP_ID"); + const guildId = getMust("DISCORD_GUILD_ID"); + + const camelToSnakeCase = (str) => + str.replace(/[A-Z]/g, (group) => + `_${group.toLowerCase()}`, + ); + + const allCommands = exports.allCommands; + const commands = Object.entries(allCommands).map(([i, j]) => ({ + name: camelToSnakeCase(i), + description: j.description, + options: j.options || null, + })); + + await restClient.put( + Routes.applicationGuildCommands(appId, guildId), + {body: commands}, + ); +} +exports.registerCommands = registerCommands; diff --git a/src/triggers/discord/interaction_create/index.js b/src/triggers/discord/interaction_create/index.js index fa491c1..c583d54 100644 --- a/src/triggers/discord/interaction_create/index.js +++ b/src/triggers/discord/interaction_create/index.js @@ -2,6 +2,8 @@ const discord = require("discord.js"); +const {allCommands} = require("./commands"); + const snakeToCamelCase = (str) => str.toLowerCase().replace(/([-_][a-z])/g, (group) => group @@ -17,11 +19,9 @@ const snakeToCamelCase = (str) => module.exports = async (interaction) => { if (!interaction.isCommand()) return; - const commands = require("./commands"); - const actionName = snakeToCamelCase(interaction.commandName); - if (actionName in commands) { - commands[actionName].action(interaction); + if (actionName in allCommands) { + allCommands[actionName].action(interaction); } else { await interaction.reply("無法存取該指令"); } diff --git a/src/triggers/discord/message_create/index.js b/src/triggers/discord/message_create/index.js index b3138b6..b29768f 100644 --- a/src/triggers/discord/message_create/index.js +++ b/src/triggers/discord/message_create/index.js @@ -2,17 +2,10 @@ const discord = require("discord.js"); -const { - useClient, -} = require("../../../clients/discord"); -const { - usePrompts, -} = require("../../../clients/gemini"); - const discordToMatrix = require("../../../bridges/discord_matrix"); -const prompts = require("../../../../prompts.json"); -const useChatSession = usePrompts(prompts); +const {useClient} = require("../../../clients/discord"); +const {chatWithAI} = require("../../../clients/openai"); /** * @param {discord.Message} message @@ -39,17 +32,16 @@ module.exports = async (message) => { return; } - const chatSession = useChatSession(message.channel.id); - let result; + let responseContent; try { - result = await chatSession.sendMessage(requestContent); + responseContent = await chatWithAI(message.channel.id, requestContent); } catch (error) { console.error(error); message.reply("思緒混亂,無法回覆。"); return; } - const responseContent = result.response.text().trim(); + responseContent = responseContent.trim(); if (!responseContent) { message.reply("無法正常回覆,請換個說法試試。"); return; diff --git a/src/triggers/matrix/room/message.js b/src/triggers/matrix/room/message.js index c171a61..40b4cf0 100644 --- a/src/triggers/matrix/room/message.js +++ b/src/triggers/matrix/room/message.js @@ -3,11 +3,8 @@ const matrixToDiscord = require("../../../bridges/matrix_discord"); const {useClient} = require("../../../clients/matrix"); -const {usePrompts} = require("../../../clients/gemini"); +const {chatWithAI} = require("../../../clients/openai"); -const prompts = require("../../../../prompts.json"); - -const useChatSession = usePrompts(prompts); const prefix = "Nymph "; /** @@ -49,10 +46,9 @@ module.exports = async (roomId, event) => { return; } - const chatSession = useChatSession(roomId); - let result; + let responseContent; try { - result = await chatSession.sendMessage(requestContent); + responseContent = await chatWithAI(roomId, requestContent); } catch (error) { console.error(error); await client.sendMessage(roomId, { @@ -63,7 +59,7 @@ module.exports = async (roomId, event) => { return; } - const responseContent = result.response.text().trim(); + responseContent = responseContent.trim(); if (!responseContent) { await client.sendMessage(roomId, { msgtype: "m.text",