schat2-clive/src/games/chat.js

298 lines
11 KiB
JavaScript

'use strict';
const config = require('config');
const bhttp = require('bhttp');
const style = require('../utils/style');
const knex = require('../knex');
const promptRegex = new RegExp(`^${config.usernamePrefix ? `${config.usernamePrefix}?` : ''}${config.user.username}[:,\\s]`, 'ui');
const settings = { ...config.chat };
const history = new Map();
const processing = new Set();
async function onStart(context) {
const totalExists = await knex.schema.hasTable('chat_tokens');
if (!totalExists) {
await knex.schema.createTable('chat_tokens', (table) => {
table.increments('id');
table.string('user_id')
.notNullable();
table.integer('tokens')
.notNullable();
table.timestamp('created')
.notNullable();
});
context.logger.info('Created database table \'chat_tokens\'');
}
const purgeResult = await knex('chat_tokens')
.where('created', '<=', knex.raw(`datetime('now', '-${config.chat.userTokenPeriod} hour')`))
.delete();
context.logger.info(`Purged ${purgeResult} expired chat token totals from database`);
}
function setHistory(value, context) {
if (!value) {
context.sendMessage(`Chat history is set to ${style.bold(settings.history)}`, context.room?.id, { label: false }, context.user.username);
return;
}
const newHistory = Number(value);
if (!Number.isNaN(newHistory) && newHistory > 0 && newHistory <= 10) {
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 }, context.user.username);
return;
}
context.sendMessage(`Chat history must be a valid number between 0 and 10, ${context.user.prefixedUsername}`, context.room?.id, { label: false }, context.user.username);
}
function setReplyWordLimit(value, context) {
if (!value) {
context.sendMessage(`Chat reply word limit is set to ${style.bold(settings.replyWordLimit)}`, context.room?.id, { label: false }, context.user.username);
return;
}
const newReplyWordLimit = Number(value);
if (!Number.isNaN(newReplyWordLimit) && newReplyWordLimit >= 3 && newReplyWordLimit <= 200) {
settings.replyWordLimit = newReplyWordLimit;
context.logger.info(`Chat reply word limit set to ${newReplyWordLimit} by ${context.user.username}`);
context.sendMessage(`Chat reply word limit set to ${style.bold(newReplyWordLimit)} by ${context.user.prefixedUsername}`, context.room?.id, { label: false }, context.user.username);
return;
}
context.sendMessage(`Chat reply word limit must be a valid number between 3 and 200, ${context.user.prefixedUsername}`, context.room?.id, { label: false }, context.user.username);
}
function setTemperature(value, context) {
if (!value) {
context.sendMessage(`Chat temperature is set to ${style.bold(settings.temperature)}`, context.room?.id, { label: false }, context.user.username);
return;
}
const newTemperature = Number(value);
if (!Number.isNaN(newTemperature) && newTemperature > 0 && newTemperature <= 2) {
settings.temperature = newTemperature;
context.logger.info(`Chat temperature set to ${newTemperature} by ${context.user.username}`);
context.sendMessage(`Chat temperature set to ${style.bold(newTemperature)} by ${context.user.prefixedUsername}`, context.room?.id, { label: false }, context.user.username);
return;
}
context.sendMessage(`Chat temperature must be a valid number between 0 and 2, ${context.user.prefixedUsername}`, context.room?.id, { label: false }, context.user.username);
}
function setModel(model, context) {
if (!model) {
context.sendMessage(`Chat model is set to ${style.bold(settings.model)}`, context.room?.id, { label: false }, context.user.username);
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 }, context.user.username);
return;
}
context.sendMessage(`Model '${model}' is not supported right now, ${context.user.prefixedUsername}`, context.room?.id, { label: false }, context.user.username);
}
function setRule(rule, context) {
if (!rule) {
context.sendMessage(`Chat is ${style.bold(settings.rule)}`, context.room?.id, { label: false }, context.user.username);
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 }, context.user.username);
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 }, context.user.username);
return;
}
context.sendMessage(`Chat rule must be at least 3 characters long, ${context.user.prefixedUsername}`, context.room?.id, { label: false }, context.user.username);
}
async function getTokens(username) {
const { used_tokens: usedTokens } = await knex('chat_tokens')
.sum('tokens as used_tokens')
.where('user_id', username)
.where('created', '>', knex.raw(`datetime('now', '-${config.chat.userTokenPeriod} hour')`)) // 1 day ago
.first();
return usedTokens || 0;
}
async function resetTokens(username) {
if (!username) {
return;
}
await knex('chat_tokens')
.where('user_id', username)
.delete();
}
async function onCommand(args, context, isConversation) {
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 (['temperature', 'temp'].includes(context.subcommand) && config.operators.includes(context.user.username)) {
setTemperature(Number(args[0]), context);
return;
}
if (['rule', 'is', 'be', 'ur'].includes(context.subcommand) && (config.chat.rulePublic || config.operators.includes(context.user.username))) {
setRule(context.subcommand === 'reset' ? 'reset' : args.join(' '), context);
return;
}
if (['limit', 'words'].includes(context.subcommand) && (config.chat.replyWordLimitPublic || config.operators.includes(context.user.username))) {
setReplyWordLimit(args[0], context);
return;
}
if (['tokens', 'credit'].includes(context.subcommand || context.command)) {
const username = args[0] ? args[0].replace(new RegExp(`^${config.usernamePrefix}`), '') : context.user.username;
const tokens = await getTokens(username);
context.sendMessage(`${args[0] ? `${style.bold(username)} has` : 'You have'} ${style.bold(config.chat.userTokenLimit - tokens)} chat tokens remaining. They will be returned gradually over ${config.chat.userTokenPeriod} hours.`, context.room?.id, { label: false }, context.user.username);
return;
}
if (context.subcommand === 'reset' && config.operators.includes(context.user.username)) {
const username = args[0] ? args[0].replace(new RegExp(`^${config.usernamePrefix}`), '') : context.user.username;
await resetTokens(username);
context.sendMessage(args[0] ? `Chat tokens reset for ${style.bold(username)}` : 'Chat tokens reset', context.room?.id, { label: false }, context.user.username);
return;
}
if (processing.has(context.user.username)) {
context.logger.info(`Skipped prompt from ${context.user.username} due processing lock`);
return;
}
const prompt = args.join(' ');
processing.add(context.user.username);
try {
const usedTokens = await getTokens(context.user.username);
if (usedTokens >= config.chat.userTokenLimit) {
context.logger.info(`${context.user.username} was rate limited at ${usedTokens}: ${prompt}`);
context.sendMessage(`Sorry, I love talking with you ${context.user.prefixedUsername}, but I need to take a break :( Check ${config.prefix}chat:tokens for more information.`, context.room?.id, { label: false }, context.user.username);
return;
}
const message = {
role: 'user',
content: settings.replyWordLimit
? `Using ${settings.replyWordLimit} words or fewer, answer as if you're ${settings.rule}. ${prompt}`
: `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,
max_tokens: config.chat.replyTokenLimit,
messages: userHistory,
user: `${context.user.id}:${context.user.username}`,
}), {
headers: {
Authorization: `Bearer ${config.chat.apiKey}`,
'Content-Type': 'application/json',
},
});
const reply = res.body.choices?.[0].message;
const target = prompt.match(/@\s*[\w-]+\s*$/)?.[0].slice(1).trim();
const curatedContent = reply.content.replace(/\n+/g, '. ');
context.logger.info(`OpenAI ${config.chat.model} replied to ${target || context.user.username} (${res.body.usage.total_tokens} tokens, ${(usedTokens || 0) + res.body.usage.total_tokens}/${config.chat.userTokenLimit} used): ${curatedContent}`);
if (isConversation) {
context.sendMessage(`${curatedContent}`, context.room?.id, { label: false }, context.user.username);
} else {
context.sendMessage(`${target ? `${config.usernamePrefix}${target}` : context.user.prefixedUsername}: ${curatedContent}`, context.room.id, { label: false });
}
await knex('chat_tokens').insert({
user_id: context.user.username,
tokens: res.body.usage.total_tokens,
created: knex.raw("datetime('now')"),
});
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 }, context.user.username);
}
processing.delete(context.user.username);
}
function onMessage(message, context) {
const isConversation = message.recipient === config.user.username && message.type !== 'whisper' && !context.containsCommand;
if (promptRegex.test(message.body) || isConversation) {
onCommand([message.body.replace(promptRegex, '').trim()], context, isConversation);
}
}
module.exports = {
onCommand,
onMessage,
onStart,
commands: ['tokens'],
};