192 lines
6.3 KiB
JavaScript
Executable File
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);
|
|
});
|
|
});
|
|
}
|