Refactor context out of chatgpt command
parent
112e960135
commit
1abaa24a2b
|
@ -3,5 +3,7 @@
|
|||
"port": 6697,
|
||||
"nick": "chatgpt",
|
||||
"channels": ["#chatgpt"],
|
||||
"openaiApiKey": "[redacted]"
|
||||
"openaiApiKey": "[redacted]",
|
||||
"initialSystemMessage": "You are a helpful assistant.",
|
||||
"alwaysRemember": false
|
||||
}
|
224
index.js
224
index.js
|
@ -4,12 +4,17 @@ 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 {
|
||||
messages = [];
|
||||
currentLine = '';
|
||||
|
||||
currentResponse = '';
|
||||
|
||||
constructor(messages = seed_messages) {
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
add_user_prompt(message) {
|
||||
this.messages.push({ role: 'user', content: message });
|
||||
}
|
||||
|
@ -18,41 +23,41 @@ class Context {
|
|||
this.messages.push({ role: 'assistant', content: message });
|
||||
}
|
||||
|
||||
append_to_line(message) {
|
||||
this.currentLine += message;
|
||||
}
|
||||
|
||||
end_line() {
|
||||
const the_line = this.currentLine;
|
||||
this.currentResponse += `${the_line}\n\n`;
|
||||
this.currentLine = '';
|
||||
return the_line;
|
||||
end_line(line) {
|
||||
this.currentResponse += `${line}\n\n`;
|
||||
return line;
|
||||
}
|
||||
|
||||
finish_current_response() {
|
||||
this.add_assistant_message(this.currentResponse);
|
||||
const theLine = this.currentLine;
|
||||
const theLine = this.currentLine;
|
||||
this.currentResponse = '';
|
||||
this.currentLine = '';
|
||||
this.save_history();
|
||||
return theLine;
|
||||
}
|
||||
|
||||
is_response_in_progress() {
|
||||
return this.currentResponse !== '' || this.currentLine !== '';
|
||||
save_history() {
|
||||
const prettyData = JSON.stringify(this.messages, null, 2);
|
||||
fs.writeFileSync('./messages.json', prettyData);
|
||||
}
|
||||
|
||||
peek_line() {
|
||||
return this.currentLine;
|
||||
is_response_in_progress() {
|
||||
return this.currentResponse !== '';
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.messages = [];
|
||||
this.currentResponse = '';
|
||||
this.currentLine = '';
|
||||
}
|
||||
}
|
||||
|
||||
const context = new Context();
|
||||
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,
|
||||
|
@ -60,101 +65,114 @@ const client = new irc.Client(config.server, config.nick, {
|
|||
|
||||
// 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) => {
|
||||
is_chat_cmd = message.startsWith('!chat');
|
||||
is_cont_cmd = message.startsWith('!cont');
|
||||
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()) { return; }
|
||||
if(is_chat_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);
|
||||
chatgpt(query, (line) => {
|
||||
client.say(to, line);
|
||||
});
|
||||
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
|
||||
async function chatgpt(query, callback) {
|
||||
// a very primitive mutex to prevent multiple calls to the api at once
|
||||
if(context.is_response_in_progress()) { return; }
|
||||
context.add_user_prompt(query);
|
||||
function chatgpt(query, messages, callback) {
|
||||
const apiUrl = 'https://api.openai.com/v1/chat/completions';
|
||||
let response = null;
|
||||
try {
|
||||
response = await axios.post(apiUrl, {
|
||||
messages: context.messages,
|
||||
model: 'gpt-3.5-turbo',
|
||||
stream: true,
|
||||
}, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.openaiApiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
responseType: 'stream',
|
||||
});
|
||||
} catch(error) {
|
||||
if (error.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
console.log(error.toJSON());
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||
// http.ClientRequest in node.js
|
||||
console.log(error.request);
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log('Error', error.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let currentLine = '';
|
||||
|
||||
response.data.on('data', (event) => {
|
||||
let data = event.toString();
|
||||
let parts = data.split('\n');
|
||||
// parse if starts with data:
|
||||
for(part of parts) {
|
||||
console.log(part);
|
||||
if(part === 'data: [DONE]') {
|
||||
callback(context.finish_current_response());
|
||||
} 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;
|
||||
}
|
||||
//split the chunk into lines leaving the delimiter in the array
|
||||
const lines = chunk.split(/\r?\n/); // split by new lines
|
||||
|
||||
let hasStartNewline = chunk.startsWith("\n");
|
||||
let hasEndNewline = chunk.endsWith("\n");
|
||||
|
||||
if(hasStartNewline) {
|
||||
callback(context.end_line())
|
||||
}
|
||||
|
||||
for (let i = 0; i < lines.length - 1; i++) {
|
||||
context.append_to_line(lines[i]);
|
||||
callback(context.end_line());
|
||||
}
|
||||
|
||||
context.append_to_line(lines[lines.length - 1]);
|
||||
|
||||
if(hasEndNewline) {
|
||||
callback(context.end_line());
|
||||
}
|
||||
|
||||
if (context.peek_line().length > 400) {
|
||||
callback(context.end_line());
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue