#!/usr/bin/env node //IRC bot that responds to !chat, queries the chatgpt api, and prints the response line by line const irc = require('irc'); const axios = require('axios'); const config = require('./config.json'); const seed_messages = [{role: 'system', content: config.initialSystemMessage}]; // context is a list of strings that are used to seed the chatgpt api and it's responses 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 = []; this.currentResponse = ''; } } let context = new Context(savedMessages); if (fs.existsSync('./messages.json')) { savedMessages = require('./messages.json'); context = new Context(savedMessages); } else { context = new Context(seed_messages); } const client = new irc.Client(config.server, config.nick, { channels: config.channels, }); // listen for messages that start with !chat and call the chatgpt api with a callback that prints the response line by line client.addListener('message', async (from, to, message) => { let is_chat_cmd = message.startsWith('!chat'); let is_cont_cmd = message.startsWith('!cont'); if (is_chat_cmd || is_cont_cmd) { if (context.is_response_in_progress()) { message(`(chat from ${from} ignored, response in progress)`) return; } if(is_chat_cmd && !config.alwaysRemember) { context.clear(); } context.add_user_prompt(query); const query = message.slice(6); try { await chatgpt(query, context.messages, handleChatGPTResponseLine); context.finish_current_response(); } catch (e) { console.log(e); client.say(to, 'Error: ' + e); } } function handleChatGPTResponseLine(line) { context.end_line(line); client.say(to, 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: 'gpt-3.5-turbo', 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) { 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); }); }); }