ircgpt/index.js

192 lines
6.3 KiB
JavaScript
Executable File

#!/usr/bin/env node
//IRC bot that responds to !chat, queries the chatgpt api, and prints the response line by line
const fs = require('fs');
const irc = require('irc-framework');
const axios = require('axios');
const config = require('./config.json');
const seed_messages = [{ role: 'system', content: config.initialSystemMessage }];
class Context {
currentResponse = '';
constructor(messages = seed_messages) {
this.messages = messages;
}
add_user_prompt(message) {
this.messages.push({ role: 'user', content: message });
}
add_assistant_message(message) {
this.messages.push({ role: 'assistant', content: message });
}
end_line(line) {
this.currentResponse += `${line}\n\n`;
return line;
}
finish_current_response() {
this.add_assistant_message(this.currentResponse);
const theLine = this.currentLine;
this.currentResponse = '';
this.save_history();
return theLine;
}
save_history() {
const prettyData = JSON.stringify(this.messages, null, 2);
fs.writeFileSync('./messages.json', prettyData);
}
is_response_in_progress() {
return this.currentResponse !== '';
}
clear() {
this.messages = [...seed_messages];
this.currentResponse = '';
}
}
let context = null;
if (fs.existsSync('./messages.json')) {
const savedMessages = require('./messages.json');
context = new Context(savedMessages);
} else {
context = new Context(seed_messages);
}
const client = new irc.Client();
client.connect({
host: config.server,
nick: config.nick,
username: config.username,
tls: config.tls,
port: config.port,
account: {
account: config.username,
password: config.password,
}
});
if(config.debug) {
client.on('debug', console.log);
client.on('raw', console.log);
}
client.on('registered', () => {
config.channels.forEach(channel => client.join(channel));
});
// listen for messages that start with !chat and call the chatgpt api with a callback that prints the response line by line
client.on('message', async (event) => {
const chatCmd = config.newChatCmd;
const contCmd = config.contChatCmd;
let is_chat_cmd = event.message.startsWith(`${chatCmd} `);
let is_cont_cmd = contCmd.length > 0 && event.message.startsWith(`${contCmd} `);
if (is_chat_cmd || is_cont_cmd) {
if (context.is_response_in_progress()) {
client.say(event.target, `(chat from ${event.nick} ignored, response in progress)`)
return;
}
let query = is_chat_cmd ? event.message.slice(chatCmd.length + 1) : event.message.slice(contCmd.length + 1);
if (is_chat_cmd && !config.alwaysRemember) {
context.clear();
}
context.add_user_prompt(query);
try {
await chatgpt(query, context.messages, handleChatGPTResponseLine);
context.finish_current_response();
} catch (e) {
console.log(e);
client.say(event.target, 'Error: ' + e);
}
}
function handleChatGPTResponseLine(line) {
context.end_line(line);
client.say(event.target, line);
}
});
// function that calls the chatgpt streaming api (with server send events) and calls the callback function for each line
function chatgpt(query, messages, callback) {
const apiUrl = 'https://api.openai.com/v1/chat/completions';
let currentLine = '';
return new Promise((resolve, reject) => {
axios.post(apiUrl, {
messages: messages,
model: config.model,
stream: true,
}, {
headers: {
Authorization: `Bearer ${config.openaiApiKey}`,
'Content-Type': 'application/json',
},
responseType: 'stream',
}).then(response => {
response.data.on('data', (event) => {
let data = event.toString();
let parts = data.split('\n');
// parse if starts with data:
for (let part of parts) {
if(config.debug) {
console.log(part);
}
if (part === 'data: [DONE]') {
callback(currentLine);
resolve();
} else if (part.startsWith('data: ')) {
let jsonString = part.slice(part.indexOf('{'), part.lastIndexOf('}') + 1);
try {
let json = JSON.parse(jsonString);
let chunk = json.choices[0].delta.content;
if (!chunk) {
continue;
}
const lines = chunk.split(/\r?\n/);
let hasStartNewline = chunk.startsWith("\n");
let hasEndNewline = chunk.endsWith("\n");
if (hasStartNewline) {
callback(currentLine);
currentLine = '';
}
for (let i = 0; i < lines.length - 1; i++) {
currentLine += lines[i];
callback(currentLine);
currentLine = '';
}
currentLine += lines[lines.length - 1];
if (hasEndNewline) {
callback(currentLine);
currentLine = '';
}
if (currentLine.length > 400) {
callback(currentLine);
currentLine = '';
}
} catch (e) {
console.log(e);
console.log(part);
}
}
}
});
}).catch(error => {
if (error.response) {
console.log(error.toJSON());
} else if (error.request) {
console.log(error.request);
} else {
console.log('Error', error.message);
}
reject(error);
});
});
}