Updated OpenAI models for chat, using proper context prompt instead of prefixing message.
This commit is contained in:
+4
-4
@@ -69,11 +69,11 @@ module.exports = {
|
|||||||
chat: {
|
chat: {
|
||||||
apiKey: null, // OpenAI
|
apiKey: null, // OpenAI
|
||||||
validModels: [
|
validModels: [
|
||||||
'gpt-3.5-turbo',
|
'gpt-6-luna',
|
||||||
'text-davinci-003',
|
'gpt-6.1-sol',
|
||||||
'gpt-4',
|
|
||||||
],
|
],
|
||||||
model: 'gpt-3.5-turbo',
|
model: 'gpt-6-luna',
|
||||||
|
reasoningEffort: 'none', // none, low, medium, high; reasoning tokens count towards the user token limit
|
||||||
userTokenLimit: 20000, // daily, roughly 100+ messages or $0.04 per user
|
userTokenLimit: 20000, // daily, roughly 100+ messages or $0.04 per user
|
||||||
userTokenPeriod: 24, // hours
|
userTokenPeriod: 24, // hours
|
||||||
replyTokenLimit: 1000,
|
replyTokenLimit: 1000,
|
||||||
|
|||||||
+38
-17
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
const config = require('config');
|
const config = require('config');
|
||||||
const bhttp = require('bhttp');
|
const bhttp = require('bhttp');
|
||||||
|
const { format } = require('date-fns');
|
||||||
|
|
||||||
const style = require('../utils/style');
|
const style = require('../utils/style');
|
||||||
const knex = require('../knex');
|
const knex = require('../knex');
|
||||||
@@ -145,6 +146,20 @@ function setRule(rule, context) {
|
|||||||
context.sendMessage(`Chat rule must be at least 3 characters long, ${context.user.prefixedUsername}`, context.room?.id, { label: false }, context.user.username);
|
context.sendMessage(`Chat rule must be at least 3 characters long, ${context.user.prefixedUsername}`, context.room?.id, { label: false }, context.user.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getInstructions(context) {
|
||||||
|
return {
|
||||||
|
role: 'developer',
|
||||||
|
content: [
|
||||||
|
`You are ${config.user.username}, ${settings.rule}, chatting in a group chat.`,
|
||||||
|
`You are talking to ${context.user.username}. Today is ${format(new Date(), 'EEEE d MMMM yyyy')}.`,
|
||||||
|
config.platform === 'irc'
|
||||||
|
? 'Reply in plain text on a single line, IRC does not support any formatting.'
|
||||||
|
: 'Reply in a single paragraph without headings or lists. You may use **bold** and *italic*.',
|
||||||
|
settings.replyWordLimit && `Use ${settings.replyWordLimit} words or fewer.`,
|
||||||
|
].filter(Boolean).join(' '),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function getTokens(username) {
|
async function getTokens(username) {
|
||||||
const { used_tokens: usedTokens } = await knex('chat_tokens')
|
const { used_tokens: usedTokens } = await knex('chat_tokens')
|
||||||
.sum('tokens as used_tokens')
|
.sum('tokens as used_tokens')
|
||||||
@@ -229,21 +244,17 @@ async function onCommand(args, context, isConversation) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = {
|
const message = { role: 'user', content: prompt };
|
||||||
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);
|
const userHistory = (history.get(context.user.username) || []).concat(message);
|
||||||
|
|
||||||
context.logger.info(`${context.user.username} asked OpenAI '${config.chat.model}' (${userHistory.length}): ${message.content}`);
|
context.logger.info(`${context.user.username} asked OpenAI '${settings.model}' (${userHistory.length}) to be ${settings.rule}: ${prompt}`);
|
||||||
|
|
||||||
const res = await bhttp.post('https://api.openai.com/v1/chat/completions', JSON.stringify({
|
const res = await bhttp.post('https://api.openai.com/v1/chat/completions', JSON.stringify({
|
||||||
model: settings.model,
|
model: settings.model,
|
||||||
max_tokens: config.chat.replyTokenLimit,
|
max_completion_tokens: config.chat.replyTokenLimit,
|
||||||
messages: userHistory,
|
reasoning_effort: config.chat.reasoningEffort,
|
||||||
|
temperature: settings.temperature,
|
||||||
|
messages: [getInstructions(context), ...userHistory], // instructions are rebuilt for every request and not stored in history, so rule changes apply immediately
|
||||||
user: `${context.user.id}:${context.user.username}`,
|
user: `${context.user.id}:${context.user.username}`,
|
||||||
}), {
|
}), {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -252,11 +263,20 @@ async function onCommand(args, context, isConversation) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const reply = res.body.choices?.[0].message;
|
if (res.statusCode !== 200) {
|
||||||
|
throw new Error(`OpenAI responded with ${res.statusCode}: ${res.body.error?.message || JSON.stringify(res.body)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reply = res.body.choices?.[0]?.message;
|
||||||
|
|
||||||
|
if (!reply?.content) {
|
||||||
|
throw new Error(`OpenAI returned no content (finish reason '${res.body.choices?.[0]?.finish_reason}', ${res.body.usage?.total_tokens} tokens)`);
|
||||||
|
}
|
||||||
|
|
||||||
const target = prompt.match(/@\s*[\w-]+\s*$/)?.[0].slice(1).trim();
|
const target = prompt.match(/@\s*[\w-]+\s*$/)?.[0].slice(1).trim();
|
||||||
const curatedContent = reply.content.replace(/\n+/g, '. ');
|
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}`);
|
context.logger.info(`OpenAI ${settings.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) {
|
if (isConversation) {
|
||||||
context.sendMessage(`${curatedContent}`, context.room?.id, { label: false }, context.user.username);
|
context.sendMessage(`${curatedContent}`, context.room?.id, { label: false }, context.user.username);
|
||||||
@@ -270,16 +290,17 @@ async function onCommand(args, context, isConversation) {
|
|||||||
created: knex.raw("datetime('now')"),
|
created: knex.raw("datetime('now')"),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (config.chat.history > 0) {
|
if (settings.history > 0) {
|
||||||
history.set(context.user.username, userHistory.concat(reply).slice(-settings.history));
|
// only keep the fields the API accepts as input, newer models return extras like refusal and annotations
|
||||||
|
history.set(context.user.username, userHistory.concat({ role: reply.role, content: reply.content }).slice(-settings.history));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
context.logger.error(error.response ? `${error.response.status}: ${JSON.stringify(error.response.data)}` : error.message);
|
context.logger.error(error.message);
|
||||||
context.sendMessage('Sorry, I can\'t think right now', context.room?.id, { label: false }, context.user.username);
|
context.sendMessage('Sorry, I can\'t think right now', context.room?.id, { label: false }, context.user.username);
|
||||||
}
|
} finally {
|
||||||
|
|
||||||
processing.delete(context.user.username);
|
processing.delete(context.user.username);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onMessage(message, context) {
|
function onMessage(message, context) {
|
||||||
const isConversation = message.recipient === config.user.username && message.type !== 'whisper' && !context.containsCommand;
|
const isConversation = message.recipient === config.user.username && message.type !== 'whisper' && !context.containsCommand;
|
||||||
|
|||||||
Reference in New Issue
Block a user