diff --git a/config/default.js b/config/default.js index e4b0f7e..b3d6b89 100755 --- a/config/default.js +++ b/config/default.js @@ -22,6 +22,7 @@ module.exports = { usernamePrefix: '@', channels: ['GamesNight'], games: [ + 'chat', 'mash', 'trivia', 'letters', @@ -61,6 +62,18 @@ module.exports = { grey: 'shadow', silver: 'shadow', }, + chat: { + apiKey: null, // OpenAI + validModels: [ + 'gpt-3.5-turbo', + 'text-davinci-003', + 'gpt-4', + ], + model: 'gpt-3.5-turbo', + history: 3, + rule: 'a tired game host', + rulePublic: true, + }, trivia: { mode: 'first', // first or timeout rounds: 10, diff --git a/src/games/8ball.js b/src/games/8ball.js index 241be5c..d9ed69d 100644 --- a/src/games/8ball.js +++ b/src/games/8ball.js @@ -94,15 +94,17 @@ async function onCommand(args, context) { purgeQuestions(); } +/* AI chat module is listening instead function onMessage(message, context) { - const regex = new RegExp(`^${config.usernamePrefix}${config.user.username}[\\s\\p{P}]*\\s+[\\w\\p{P}]+\\s+[\\w, \\p{P}]+.*\\?`, 'ui'); + const regex = new RegExp(`^${config.usernamePrefix}?${config.user.username}[\\s\\p{P}]*\\s+[\\w\\p{P}]+\\s+[\\w, \\p{P}]+.*\\?`, 'ui'); if (regex.test(message.body)) { onCommand([message.body.replaceAll(`${config.usernamePrefix}${config.user.username}:?`, '').trim()], context, false); } } +*/ module.exports = { onCommand, - onMessage, + // onMessage, }; diff --git a/src/games/chat.js b/src/games/chat.js new file mode 100644 index 0000000..20e2467 --- /dev/null +++ b/src/games/chat.js @@ -0,0 +1,139 @@ +'use strict'; + +const config = require('config'); +const bhttp = require('bhttp'); + +const style = require('../utils/style'); + +const promptRegex = new RegExp(`^${config.usernamePrefix}?${config.user.username}[:,\\s]`, 'ui'); +const history = new Map(); + +const settings = config.chat; + +function setHistory(value, context) { + if (!value) { + context.sendMessage(`Chat history is set to ${style.bold(settings.history)}`, context.room.id, { label: false }); + return; + } + + const newHistory = Number(value); + + if (!Number.isNaN(newHistory)) { + settings.history = newHistory; + + context.logger.info(`Chat history set to ${newHistory} by ${context.user.username}`); + context.sendMessage(`Chat history set to ${style.bold(newHistory)} by ${context.user.prefixedUsername}`, context.room.id, { label: false }); + + return; + } + + context.logger.info(`Chat history must be a valid number, ${context.user.prefixedUsername}`); +} + +function setModel(model, context) { + if (!model) { + context.sendMessage(`Chat model is set to ${style.bold(settings.model)}`, context.room.id, { label: false }); + return; + } + + if (config.chat.validModels.includes(model)) { + settings.model = model; + + context.logger.info(`Chat model set to '${model}' by ${context.user.username}`); + context.sendMessage(`Chat model set to '${style.bold(model)}' by ${context.user.prefixedUsername}`, context.room.id, { label: false }); + + return; + } + + context.logger.info(`Model '${model}' is not supported right now, ${context.user.prefixedUsername}`); +} + +function setRule(rule, context) { + if (!rule) { + context.sendMessage(`Chat is ${style.bold(settings.rule)}`, context.room.id, { label: false }); + return; + } + + if (rule === 'reset') { + settings.rule = config.chat.rule; + + context.logger.info(`Chat reset by ${context.user.username} to be ${config.chat.rule}`); + context.sendMessage(`Chat reset by ${context.user.prefixedUsername} to be ${style.bold(config.chat.rule)}`, context.room.id, { label: false }); + + return; + } + + if (rule.length > 3) { + settings.rule = `${rule.replace(/\.+$/, '')}`; + + context.logger.info(`Chat set by ${context.user.username} to be ${rule}`); + context.sendMessage(`Chat set by ${context.user.prefixedUsername} to be ${style.bold(rule)}`, context.room.id, { label: false }); + + return; + } + + context.sendMessage(`Chat rule must be at least 3 characters long, ${context.user.prefixedUsername}`, context.room.id, { label: false }); +} + +async function onCommand(args, context) { + if (context.subcommand === 'history' && config.operators.includes(context.user.username)) { + setHistory(args[0], context); + return; + } + + if (context.subcommand === 'model' && config.operators.includes(context.user.username)) { + setModel(args[0], context); + return; + } + + if (['rule', 'is', 'reset'].includes(context.subcommand) && (config.chat.rulePublic || config.operators.includes(context.user.username))) { + setRule(context.subcommand === 'reset' ? 'reset' : args.join(' '), context); + return; + } + + const prompt = args.join(' '); + + try { + const message = { + role: 'user', + content: `Answer as if you're ${settings.rule}. ${prompt}`, + }; + + const userHistory = (history.get(context.user.username) || []).concat(message); + + context.logger.info(`${context.user.username} asked OpenAI '${config.chat.model}' (${userHistory.length}): ${message.content}`); + + const res = await bhttp.post('https://api.openai.com/v1/chat/completions', JSON.stringify({ + model: settings.model, + messages: userHistory, + }), { + headers: { + Authorization: `Bearer ${config.chat.apiKey}`, + 'Content-Type': 'application/json', + }, + }); + + const reply = res.body.choices?.[0].message; + + context.logger.info(`OpenAI ${config.chat.model} replied to ${context.user.username} (${res.body.usage.total_tokens} tokens): ${reply.content}`); + context.sendMessage(`${context.user.prefixedUsername}: ${reply.content}`, context.room.id, { label: false }); + + if (config.chat.history > 0) { + history.set(context.user.username, userHistory.concat(reply).slice(-settings.history)); + } + } catch (error) { + context.logger.error(error.response ? `${error.response.status}: ${JSON.stringify(error.response.data)}` : error.message); + context.sendMessage('Sorry, I can\'t think right now', context.room.id, { label: false }); + } +} + +function onMessage(message, context) { + if (promptRegex.test(message.body)) { + onCommand([message.body.replace(promptRegex, '').trim()], context); + } +} + +module.exports = { + onCommand, + onMessage, +};