#!/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); }); }); }