Files
SillyTavern/public/scripts/openai.js
2023-03-28 17:44:28 +03:00

674 lines
25 KiB
JavaScript

/*
* CODE FOR OPENAI SUPPORT
* By CncAnon (@CncAnon1)
* https://github.com/CncAnon1/TavernAITurbo
*/
import {
saveSettingsDebounced,
addOneMessage,
messageFormating,
substituteParams,
count_view_mes,
saveChat,
checkOnlineStatus,
setOnlineStatus,
getExtensionPrompt,
token,
name1,
name2,
extension_prompt_types,
} from "../script.js";
import { groups, selected_group } from "./group-chats.js";
import {
pin_examples,
} from "./power-user.js";
export {
is_get_status_openai,
openai_msgs,
oai_settings,
loadOpenAISettings,
setOpenAIMessages,
setOpenAIMessageExamples,
generateOpenAIPromptCache,
prepareOpenAIMessages,
sendOpenAIRequest,
setOpenAIOnlineStatus,
}
let openai_msgs = [];
let openai_msgs_example = [];
let is_get_status_openai = false;
let is_api_button_press_openai = false;
const default_main_prompt = "Write {{char}}'s next reply in a fictional chat between {{char}} and {{user}}. Write 1 reply only in internet RP style, italicize actions, and avoid quotation marks. Use markdown. Be proactive, creative, and drive the plot and conversation forward. Write at least 1 paragraph, up to 4. Always stay in character and avoid repetition.";
const default_nsfw_prompt = "NSFW/Smut is allowed. Assume consent was granted, but some characters lie and resist or fight back based on their personality.";
const gpt3_max = 4095;
const gpt4_max = 8191;
const oai_settings = {
preset_settings_openai: 'Default',
api_key_openai: '',
temp_openai: 1.0,
freq_pen_openai: 0,
pres_pen_openai: 0,
stream_openai: false,
openai_max_context: gpt3_max,
openai_max_tokens: 300,
nsfw_toggle: true,
enhance_definitions: false,
wrap_in_quotes: false,
nsfw_first: false,
main_prompt: default_main_prompt,
nsfw_prompt: default_nsfw_prompt,
openai_model: 'gpt-3.5-turbo-0301',
};
let openai_setting_names;
let openai_settings;
function setOpenAIOnlineStatus(value) {
is_get_status_openai = value;
}
function setOpenAIMessages(chat) {
let j = 0;
// clean openai msgs
openai_msgs = [];
for (let i = chat.length - 1; i >= 0; i--) {
// first greeting message
if (j == 0) {
chat[j]['mes'] = substituteParams(chat[j]['mes']);
}
let role = chat[j]['is_user'] ? 'user' : 'assistant';
let content = chat[j]['mes'];
// for groups - prepend a character's name
if (selected_group) {
content = `${chat[j].name}: ${content}`;
}
// system messages produce no content
if (chat[j]['is_system']) {
role = 'system';
content = '';
}
// replace bias markup
content = (content ?? '').replace(/{.*}/g, '');
// Apply the "wrap in quotes" option
if (role == 'user' && oai_settings.wrap_in_quotes) content = `"${content}"`;
openai_msgs[i] = { "role": role, "content": content };
j++;
}
for (let i = 1; i < 100; i++) {
const anchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, i);
if (anchor && anchor.length) {
openai_msgs.splice(i, 0, { "role": 'system', 'content': anchor.trim() })
}
}
}
function setOpenAIMessageExamples(mesExamplesArray) {
// get a nice array of all blocks of all example messages = array of arrays (important!)
openai_msgs_example = [];
for (let item of mesExamplesArray) {
// remove <START> {Example Dialogue:} and replace \r\n with just \n
let replaced = item.replace(/<START>/i, "{Example Dialogue:}").replace(/\r/gm, '');
let parsed = parseExampleIntoIndividual(replaced);
// add to the example message blocks array
openai_msgs_example.push(parsed);
}
}
function generateOpenAIPromptCache(charPersonality, topAnchorDepth, anchorTop, anchorBottom) {
openai_msgs = openai_msgs.reverse();
let is_add_personality = false;
openai_msgs.forEach(function (msg, i, arr) {//For added anchors and others
let item = msg["content"];
if (i === openai_msgs.length - topAnchorDepth && count_view_mes >= topAnchorDepth && !is_add_personality) {
is_add_personality = true;
if ((anchorTop != "" || charPersonality != "")) {
if (anchorTop != "") charPersonality += ' ';
item = `[${name2} is ${charPersonality}${anchorTop}]\n${item}`;
}
}
if (i >= openai_msgs.length - 1 && count_view_mes > 8 && $.trim(item).substr(0, (name1 + ":").length) == name1 + ":") {//For add anchor in end
item = anchorBottom + "\n" + item;
}
msg["content"] = item;
openai_msgs[i] = msg;
});
}
function parseExampleIntoIndividual(messageExampleString) {
let result = []; // array of msgs
let tmp = messageExampleString.split("\n");
let cur_msg_lines = [];
let in_user = false;
let in_bot = false;
// DRY my cock and balls
function add_msg(name, role) {
// join different newlines (we split them by \n and join by \n)
// remove char name
// strip to remove extra spaces
let parsed_msg = cur_msg_lines.join("\n").replace(name + ":", "").trim();
if (selected_group && role == 'assistant') {
parsed_msg = `${name}: ${parsed_msg}`;
}
result.push({ "role": role, "content": parsed_msg });
cur_msg_lines = [];
}
// skip first line as it'll always be "This is how {bot name} should talk"
for (let i = 1; i < tmp.length; i++) {
let cur_str = tmp[i];
// if it's the user message, switch into user mode and out of bot mode
// yes, repeated code, but I don't care
if (cur_str.indexOf(name1 + ":") === 0) {
in_user = true;
// we were in the bot mode previously, add the message
if (in_bot) {
add_msg(name2, "assistant");
}
in_bot = false;
} else if (cur_str.indexOf(name2 + ":") === 0) {
in_bot = true;
// we were in the user mode previously, add the message
if (in_user) {
add_msg(name1, "user");
}
in_user = false;
}
// push the current line into the current message array only after checking for presence of user/bot
cur_msg_lines.push(cur_str);
}
// Special case for last message in a block because we don't have a new message to trigger the switch
if (in_user) {
add_msg(name1, "user");
} else if (in_bot) {
add_msg(name2, "assistant");
}
return result;
}
function formatWorldInfo(value) {
if (!value) {
return '';
}
// placeholder if we would want to apply some formatting
return `[Details of the fictional world the RP set in:\n${value}\n]`;
}
async function prepareOpenAIMessages(name2, storyString, worldInfoBefore, worldInfoAfter, extensionPrompt, bias) {
let this_max_context = oai_settings.openai_max_context;
let nsfw_toggle_prompt = "";
let enhance_definitions_prompt = "";
if (oai_settings.nsfw_toggle) {
nsfw_toggle_prompt = oai_settings.nsfw_prompt;
} else {
nsfw_toggle_prompt = "Avoid writing a NSFW/Smut reply. Creatively write around it NSFW/Smut scenarios in character.";
}
// Experimental but kinda works
if (oai_settings.enhance_definitions) {
enhance_definitions_prompt = "If you have more knowledge of " + name2 + ", add to the character's lore and personality to enhance them but keep the Character Sheet's definitions absolute.";
}
let whole_prompt = [];
// If it's toggled, NSFW prompt goes first.
if (oai_settings.nsfw_first) {
whole_prompt = [nsfw_toggle_prompt, oai_settings.main_prompt, enhance_definitions_prompt, "\n\n", formatWorldInfo(worldInfoBefore), storyString, formatWorldInfo(worldInfoAfter), extensionPrompt]
}
else {
whole_prompt = [oai_settings.main_prompt, nsfw_toggle_prompt, enhance_definitions_prompt, "\n\n", formatWorldInfo(worldInfoBefore), storyString, formatWorldInfo(worldInfoAfter), extensionPrompt]
}
// Join by a space and replace placeholders with real user/char names
storyString = substituteParams(whole_prompt.join(" "))
let prompt_msg = { "role": "system", "content": storyString }
let examples_tosend = [];
let openai_msgs_tosend = [];
// todo: static value, maybe include in the initial context calculation
let new_chat_msg = { "role": "system", "content": "[Start a new chat]" };
let start_chat_count = await countTokens([new_chat_msg]);
let total_count = await countTokens([prompt_msg], true) + start_chat_count;
if (bias && bias.trim().length) {
let bias_msg = { "role": "system", "content": bias.trim() };
openai_msgs.push(bias_msg);
total_count += await countTokens([bias_msg], true);
}
if (selected_group) {
// set "special" group nudging messages
const groupMembers = groups.find(x => x.id === selected_group)?.members;
const names = Array.isArray(groupMembers) ? groupMembers.join(', ') : '';
new_chat_msg.content = `[Start a new group chat. Group members: ${names}]`;
let group_nudge = { "role": "system", "content": `[Write the next reply only as ${name2}]` };
openai_msgs.push(group_nudge);
// add a group nudge count
let group_nudge_count = await countTokens([group_nudge], true);
total_count += group_nudge_count;
// recount tokens for new start message
total_count -= start_chat_count
start_chat_count = await countTokens([new_chat_msg]);
total_count += start_chat_count;
}
// The user wants to always have all example messages in the context
if (pin_examples) {
// first we send *all* example messages
// we don't check their token size since if it's bigger than the context, the user is fucked anyway
// and should've have selected that option (maybe have some warning idk, too hard to add)
for (const element of openai_msgs_example) {
// get the current example block with multiple user/bot messages
let example_block = element;
// add the first message from the user to tell the model that it's a new dialogue
// TODO: instead of role user content use role system name example_user
// message from the user so the model doesn't confuse the context (maybe, I just think that this should be done)
if (example_block.length != 0) {
examples_tosend.push(new_chat_msg);
}
for (const example of example_block) {
// add all the messages from the example
examples_tosend.push(example);
}
}
total_count += await countTokens(examples_tosend);
// go from newest message to oldest, because we want to delete the older ones from the context
for (let j = openai_msgs.length - 1; j >= 0; j--) {
let item = openai_msgs[j];
let item_count = await countTokens(item);
// If we have enough space for this message, also account for the max assistant reply size
if ((total_count + item_count) < (this_max_context - oai_settings.openai_max_tokens)) {
openai_msgs_tosend.push(item);
total_count += item_count;
}
else {
// early break since if we still have more messages, they just won't fit anyway
break;
}
}
} else {
for (let j = openai_msgs.length - 1; j >= 0; j--) {
let item = openai_msgs[j];
let item_count = await countTokens(item);
// If we have enough space for this message, also account for the max assistant reply size
if ((total_count + item_count) < (this_max_context - oai_settings.openai_max_tokens)) {
openai_msgs_tosend.push(item);
total_count += item_count;
}
else {
// early break since if we still have more messages, they just won't fit anyway
break;
}
}
console.log(total_count);
for (const example of openai_msgs_example) {
// get the current example block with multiple user/bot messages
let example_block = example;
for (let k = 0; k < example_block.length; k++) {
if (example_block.length == 0) { continue; }
let example_count = await countTokens(example_block[k]);
// add all the messages from the example
if ((total_count + example_count + start_chat_count) < (this_max_context - oai_settings.openai_max_tokens)) {
if (k == 0) {
examples_tosend.push(new_chat_msg);
total_count += start_chat_count;
}
examples_tosend.push(example_block[k]);
total_count += example_count;
}
else { break; }
}
}
}
// reverse the messages array because we had the newest at the top to remove the oldest,
// now we want proper order
openai_msgs_tosend.reverse();
openai_msgs_tosend = [prompt_msg, ...examples_tosend, new_chat_msg, ...openai_msgs_tosend]
console.log("We're sending this:")
console.log(openai_msgs_tosend);
console.log(`Calculated the total context to be ${total_count} tokens`);
return openai_msgs_tosend;
}
async function sendOpenAIRequest(openai_msgs_tosend) {
const generate_data = {
"messages": openai_msgs_tosend,
"model": oai_settings.openai_model,
"temperature": parseFloat(oai_settings.temp_openai),
"frequency_penalty": parseFloat(oai_settings.freq_pen_openai),
"presence_penalty": parseFloat(oai_settings.pres_pen_openai),
"max_tokens": oai_settings.openai_max_tokens,
"stream": false, //oai_settings.stream_openai,
};
const generate_url = '/generate_openai';
// TODO: fix streaming
const streaming = oai_settings.stream_openai;
const last_view_mes = count_view_mes;
const response = await fetch(generate_url, {
method: 'POST',
body: JSON.stringify(generate_data),
headers: {
'Content-Type': 'application/json',
"X-CSRF-Token": token,
}
});
const data = await response.json();
if (data.error) {
throw new Error(data);
}
return data.choices[0]["message"]["content"];
}
// Unused
function onStream(e, resolve, reject, last_view_mes) {
let end = false;
if (!oai_settings.stream_openai)
return;
let response = e.currentTarget.response;
if (response == "{\"error\":true}") {
reject('', 'error');
}
let eventList = response.split("\n");
let getMessage = "";
for (let event of eventList) {
if (!event.startsWith("data"))
continue;
if (event == "data: [DONE]") {
chat[chat.length - 1]['mes'] = getMessage;
$("#send_but").css("display", "block");
$("#loading_mes").css("display", "none");
saveChat();
end = true;
break;
}
let data = JSON.parse(event.substring(6));
// the first and last messages are undefined, protect against that
getMessage += data.choices[0]["delta"]["content"] || "";
}
if ($("#chat").children().filter(`[mesid="${last_view_mes}"]`).length == 0) {
chat[chat.length] = {};
chat[chat.length - 1]['name'] = name2;
chat[chat.length - 1]['is_user'] = false;
chat[chat.length - 1]['is_name'] = false;
chat[chat.length - 1]['send_date'] = Date.now();
chat[chat.length - 1]['mes'] = "";
addOneMessage(chat[chat.length - 1]);
}
let messageText = messageFormating($.trim(getMessage), name1);
$("#chat").children().filter(`[mesid="${last_view_mes}"]`).children('.mes_block').children('.mes_text').html(messageText);
let $textchat = $('#chat');
$textchat.scrollTop($textchat[0].scrollHeight);
if (end) {
resolve();
}
}
async function countTokens(messages, full = false) {
return new Promise((resolve) => {
if (!Array.isArray(messages)) {
messages = [messages];
}
let token_count = -1;
jQuery.ajax({
async: true,
type: 'POST', //
url: `/tokenize_openai?model=${oai_settings.openai_model}`,
data: JSON.stringify(messages),
dataType: "json",
contentType: "application/json",
success: function (data) {
token_count = data.token_count;
if (!full) token_count -= 2;
resolve(token_count);
}
});
});
}
function loadOpenAISettings(data, settings) {
if (settings.api_key_openai != undefined) {
oai_settings.api_key_openai = settings.api_key_openai;
$("#api_key_openai").val(oai_settings.api_key_openai);
}
openai_setting_names = data.openai_setting_names;
openai_settings = data.openai_settings;
openai_settings = data.openai_settings;
openai_settings.forEach(function (item, i, arr) {
openai_settings[i] = JSON.parse(item);
});
$("#settings_perset_openai").empty();
let arr_holder = {};
openai_setting_names.forEach(function (item, i, arr) {
arr_holder[item] = i;
$('#settings_perset_openai').append(`<option value=${i}>${item}</option>`);
});
openai_setting_names = arr_holder;
oai_settings.preset_settings_openai = settings.preset_settings_openai;
$(`#settings_perset_openai option[value=${openai_setting_names[oai_settings.preset_settings_openai]}]`).attr('selected', true);
oai_settings.temp_openai = settings.temp_openai ?? 0.9;
oai_settings.freq_pen_openai = settings.freq_pen_openai ?? 0.7;
oai_settings.pres_pen_openai = settings.pres_pen_openai ?? 0.7;
oai_settings.stream_openai = settings.stream_openai ?? true;
oai_settings.openai_max_context = settings.openai_max_context ?? 4095;
oai_settings.openai_max_tokens = settings.openai_max_tokens ?? 300;
if (settings.nsfw_toggle !== undefined) oai_settings.nsfw_toggle = !!settings.nsfw_toggle;
if (settings.keep_example_dialogue !== undefined) oai_settings.keep_example_dialogue = !!settings.keep_example_dialogue;
if (settings.enhance_definitions !== undefined) oai_settings.enhance_definitions = !!settings.enhance_definitions;
if (settings.wrap_in_quotes !== undefined) oai_settings.wrap_in_quotes = !!settings.wrap_in_quotes;
if (settings.nsfw_first !== undefined) oai_settings.nsfw_first = !!settings.nsfw_first;
if (settings.openai_model !== undefined) oai_settings.openai_model = settings.openai_model;
$('#stream_toggle').prop('checked', oai_settings.stream_openai);
$(`#model_openai_select option[value="${oai_settings.openai_model}"`).attr('selected', true).trigger('change');
$('#openai_max_context').val(oai_settings.openai_max_context);
$('#openai_max_context_counter').text(`${oai_settings.openai_max_context} Tokens`);
$('#openai_max_tokens').val(oai_settings.openai_max_tokens);
$('#nsfw_toggle').prop('checked', oai_settings.nsfw_toggle);
$('#keep_example_dialogue').prop('checked', oai_settings.keep_example_dialogue);
$('#enhance_definitions').prop('checked', oai_settings.enhance_definitions);
$('#wrap_in_quotes').prop('checked', oai_settings.wrap_in_quotes);
$('#nsfw_first').prop('checked', oai_settings.nsfw_first);
if (settings.main_prompt !== undefined) oai_settings.main_prompt = settings.main_prompt;
if (settings.nsfw_prompt !== undefined) oai_settings.nsfw_prompt = settings.nsfw_prompt;
$('#main_prompt_textarea').val(oai_settings.main_prompt);
$('#nsfw_prompt_textarea').val(oai_settings.nsfw_prompt);
$('#temp_openai').val(oai_settings.temp_openai);
$('#temp_counter_openai').text(Number(oai_settings.temp_openai).toFixed(2));
$('#freq_pen_openai').val(oai_settings.freq_pen_openai);
$('#freq_pen_counter_openai').text(Number(oai_settings.freq_pen_openai).toFixed(2));
$('#pres_pen_openai').val(oai_settings.pres_pen_openai);
$('#pres_pen_counter_openai').text(Number(oai_settings.pres_pen_openai).toFixed(2));
}
async function getStatusOpen() {
if (is_get_status_openai) {
let data = { key: oai_settings.api_key_openai };
jQuery.ajax({
type: 'POST', //
url: '/getstatus_openai', //
data: JSON.stringify(data),
beforeSend: function () { },
cache: false,
dataType: "json",
contentType: "application/json",
success: function (data) {
if (!('error' in data))
setOnlineStatus('Valid');
resultCheckStatusOpen();
},
error: function (jqXHR, exception) {
setOnlineStatus('no_connection');
console.log(exception);
console.log(jqXHR);
resultCheckStatusOpen();
}
});
} else {
setOnlineStatus('no_connection');
}
}
function resultCheckStatusOpen() {
is_api_button_press_openai = false;
checkOnlineStatus();
$("#api_loading_openai").css("display", 'none');
$("#api_button_openai").css("display", 'inline-block');
}
$(document).ready(function () {
$(document).on('input', '#temp_openai', function () {
oai_settings.temp_openai = $(this).val();
$('#temp_counter_openai').text(Number($(this).val()).toFixed(2));
saveSettingsDebounced();
});
$(document).on('input', '#freq_pen_openai', function () {
oai_settings.freq_pen_openai = $(this).val();
$('#freq_pen_counter_openai').text(Number($(this).val()).toFixed(2));
saveSettingsDebounced();
});
$(document).on('input', '#pres_pen_openai', function () {
oai_settings.pres_pen_openai = $(this).val();
$('#pres_pen_counter_openai').text(Number($(this).val()));
saveSettingsDebounced();
});
$(document).on('input', '#openai_max_context', function () {
oai_settings.openai_max_context = parseInt($(this).val());
$('#openai_max_context_counter').text(`${$(this).val()} Tokens`);
saveSettingsDebounced();
});
$(document).on('input', '#openai_max_tokens', function () {
oai_settings.openai_max_tokens = parseInt($(this).val());
saveSettingsDebounced();
});
$("#model_openai_select").change(function() {
const value = $(this).val();
oai_settings.openai_model = value;
if (value == 'gpt-4') {
$('#openai_max_context').attr('max', gpt4_max);
}
else {
$('#openai_max_context').attr('max', gpt3_max);
oai_settings.openai_max_context = Math.max(oai_settings.openai_max_context, gpt3_max);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
}
saveSettingsDebounced();
});
$('#stream_toggle').change(function () {
oai_settings.stream_openai = !!$('#stream_toggle').prop('checked');
saveSettingsDebounced();
});
$('#nsfw_toggle').change(function () {
oai_settings.nsfw_toggle = !!$('#nsfw_toggle').prop('checked');
saveSettingsDebounced();
});
$('#enhance_definitions').change(function () {
oai_settings.enhance_definitions = !!$('#enhance_definitions').prop('checked');
saveSettingsDebounced();
});
$('#wrap_in_quotes').change(function () {
oai_settings.wrap_in_quotes = !!$('#wrap_in_quotes').prop('checked');
saveSettingsDebounced();
});
$('#nsfw_first').change(function () {
oai_settings.nsfw_first = !!$('#nsfw_first').prop('checked');
saveSettingsDebounced();
});
$("#settings_perset_openai").change(function () {
oai_settings.preset_settings_openai = $('#settings_perset_openai').find(":selected").text();
const preset = openai_settings[openai_setting_names[preset_settings_openai]];
oai_settings.temp_openai = preset.temperature;
oai_settings.freq_pen_openai = preset.frequency_penalty;
oai_settings.pres_pen_openai = preset.presence_penalty;
// probably not needed
$('#temp_counter_openai').text(oai_settings.temp_openai);
$('#freq_pen_counter_openai').text(oai_settings.freq_pen_openai);
$('#pres_pen_counter_openai').text(oai_settings.pres_pen_openai);
$('#temp_openai').val(oai_settings.temp_openai).trigger('input');
$('#freq_pen_openai').val(oai_settings.freq_pen_openai).trigger('input');
$('#pres_pen_openai').val(oai_settings.pres_pen_openai).trigger('input');
saveSettingsDebounced();
});
$("#api_button_openai").click(function (e) {
e.stopPropagation();
if ($('#api_key_openai').val() != '') {
$("#api_loading_openai").css("display", 'inline-block');
$("#api_button_openai").css("display", 'none');
oai_settings.api_key_openai = $.trim($('#api_key_openai').val());
saveSettingsDebounced();
is_get_status_openai = true;
is_api_button_press_openai = true;
getStatusOpen();
}
});
$("#save_prompts").click(function () {
oai_settings.main_prompt = $('#main_prompt_textarea').val();
oai_settings.nsfw_prompt = $('#nsfw_prompt_textarea').val();
saveSettingsDebounced();
});
});