Merge pull request #1 from SillyLossy/worldinfo

Worldinfo
This commit is contained in:
SillyLossy
2023-02-12 00:20:30 +02:00
committed by GitHub
6 changed files with 1354 additions and 10 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

View File

@@ -0,0 +1,30 @@
{
"folders": {
"Sample Folder": [
0,
1
]
},
"entries": {
"0": {
"uid": 0,
"title": "AAA",
"key": [ "AAA" ],
"keysecondary": [ ],
"constant": false,
"content": "AAA is a city where BBB lives.",
"comment": "AAA definition",
"selective": true
},
"1": {
"uid": 1,
"title": "BBB",
"key": [ "BBB" ],
"keysecondary": [ ],
"constant": false,
"content": "BBB is a 21-year old female student of CCC academy.",
"comment": "BBB definition",
"selective": true
}
}
}

View File

@@ -54,6 +54,7 @@
var is_mes_reload_avatar = false;
var is_advanced_char_open = false;
var is_world_edit_open = false;
var menu_type = '';//what is selected in the menu
var selected_button = '';//which button pressed
@@ -67,6 +68,8 @@
var create_save_mes_example = '';
var timerSaveEdit;
var timerKoboldSync;
var timerWorldSave;
var durationSaveEdit = 200;
//animation right menu
var animation_rm_duration = 200;
@@ -102,6 +105,13 @@
var user_avatar = 'you.png';
var temp = 0.5;
var amount_gen = 80;
var kobold_world = null;
var koboldai_world_names;
var kobold_world_synced = false;
var kobold_sync_failed = false;
var kobold_is_united = false;
var kobold_world_data = null;
var imported_world_name = '';
var max_context = 2048;//2048;
var rep_pen = 1;
var rep_pen_size = 100;
@@ -191,7 +201,6 @@
$("#online_status_indicator3").css("background-color", "green");
$("#online_status_text3").html(online_status);
}
}
async function getLastVersion(){
@@ -251,6 +260,9 @@
online_status = data.result;
if(online_status == undefined){
online_status = 'no_connection';
kobold_world_synced = false;
updateWorldStatus();
}
if(online_status.toLowerCase().indexOf('pygmalion') != -1){
is_pygmalion = true;
@@ -263,6 +275,7 @@
resultCheckStatus();
if(online_status !== 'no_connection'){
var checkStatusNow = setTimeout(getStatus, 3000);//getStatus();
syncKoboldWorldInfo(false);
}
},
error: function (jqXHR, exception) {
@@ -270,12 +283,18 @@
console.log(jqXHR);
online_status = 'no_connection';
// invalidate world info when losing connection to kobold
kobold_world_synced = false;
updateWorldStatus();
resultCheckStatus();
}
});
}else{
if(is_get_status_novel != true){
online_status = 'no_connection';
kobold_world_synced = false;
updateWorldStatus();
}
}
}
@@ -896,7 +915,8 @@
var generate_data;
if(main_api == 'kobold'){
var generate_data = {prompt: finalPromt, gui_settings: true,max_length: amount_gen,temperature: temp, max_context_length: max_context};
const use_world_info = Boolean(kobold_world && kobold_world_synced);
var generate_data = {prompt: finalPromt, gui_settings: true,max_length: amount_gen,temperature: temp, max_context_length: max_context, use_world_info};
if(preset_settings != 'gui'){
var this_settings = koboldai_settings[koboldai_setting_names[preset_settings]];
@@ -937,7 +957,8 @@
s4:this_settings.sampler_order[3],
s5:this_settings.sampler_order[4],
s6:this_settings.sampler_order[5],
s7:this_settings.sampler_order[6]
s7:this_settings.sampler_order[6],
use_world_info: use_world_info,
};
}
}
@@ -1199,6 +1220,9 @@
$( "#rm_button_characters" ).children("h2").css(deselected_button_style);
$( "#rm_button_settings" ).children("h2").css(seleced_button_style);
$( "#rm_button_selected_ch" ).children("h2").css(deselected_button_style);
// Dumb call, but won't need an interval
updateWorldStatus();
});
$( "#rm_button_characters" ).click(function() {
selected_button = 'characters';
@@ -1504,6 +1528,17 @@
}
});
}
if (popup_type === 'world_imported' && imported_world_name) {
koboldai_world_names.forEach((item, i) => {
if (item === imported_world_name) {
$('#world_info').val(i).change();
}
})
imported_world_name = '';
}
if (popup_type === 'del_world' && kobold_world) {
deleteWorldInfo(kobold_world);
}
if(popup_type == 'new_chat' && this_chid != undefined && menu_type != "create"){//Fix it; New chat doesn't create while open create character menu
clearChat();
chat.length = 0;
@@ -1529,11 +1564,13 @@
$("#dialogue_popup_cancel").css("display", "none");
break;
case 'world_imported':
case 'new_chat':
$("#dialogue_popup_ok").css("background-color", "#191b31CC");
$("#dialogue_popup_ok").text("Yes");
break;
case 'del_world':
default:
$("#dialogue_popup_ok").css("background-color", "#791b31");
$("#dialogue_popup_ok").text("Delete");
@@ -1829,6 +1866,7 @@
is_get_status = true;
is_api_button_press = true;
getStatus();
detectUnitedKobold();
}
});
@@ -1927,6 +1965,23 @@
});
$("#world_info").change(function() {
const selectedWorld = $('#world_info').find(":selected").val();
kobold_world_synced = false;
kobold_sync_failed = false;
kobold_world = null;
if (selectedWorld !== 'None') {
const worldIndex = Number(selectedWorld);
kobold_world = !isNaN(worldIndex) ? koboldai_world_names[worldIndex] : null;
}
hideWorldEditor();
syncKoboldWorldInfo(true);
saveSettings();
updateWorldStatus();
});
$( "#settings_perset" ).change(function() {
if($('#settings_perset').find(":selected").val() != 'gui'){
@@ -2005,6 +2060,7 @@
main_api = 'kobold';
$('#max_context_block').css('display', 'block');
$('#amount_gen_block').css('display', 'block');
$('#world_info_block').css('display', 'flex');
}
if($('#main_api').find(":selected").val() == 'novel'){
$('#kobold_api').css("display", "none");
@@ -2012,7 +2068,10 @@
main_api = 'novel';
$('#max_context_block').css('display', 'none');
$('#amount_gen_block').css('display', 'none');
$('#world_info_block').css('display', 'none');
}
updateWorldStatus();
}
async function getUserAvatars(){
const response = await fetch("/getuseravatars", {
@@ -2037,7 +2096,100 @@
}
}
function updateWorldStatus() {
if($('#world_info_block').is(':visible') && kobold_world) {
$('#world_info_edit_button').show();
$('#world_status').show();
if (kobold_world_synced) {
$("#world_status_indicator").css("background-color", "green");
$("#world_status_text").html("Synchronized with KoboldAI")
}
else {
let statusText = online_status === 'no_connection'
? "Waiting for connection"
: "Synchronizing...";
if (kobold_sync_failed) {
statusText = "Synchronization failed (see console)";
}
$("#world_status_text").html(statusText);
$("#world_status_indicator").css("background-color", "red");
}
if (kobold_is_united) {
$("#world_status_text").html('<span style="font-size:90%">KoboldAI United detected. WI may not work as intended.<br>If experiencing issues, please select "None".</span>');
}
} else {
$('#world_status').hide();
$('#world_info_edit_button').hide();
}
}
async function detectUnitedKobold() {
if (!api_server || main_api !== 'kobold') {
return;
}
// If we can reach Kobold's new ui, then it should be United branch
kobold_is_united = false;
try {
const kobold_united_ui2 = api_server.replace('/api', '/new_ui');
const response = await fetch(kobold_united_ui2, { method: 'HEAD'});
if (response.ok && response.status == 200) {
kobold_is_united = true;
}
}
catch {
// empty catch
}
}
async function syncKoboldWorldInfo(force) {
// Don't sync if no world selected or if synced and not forcing
if (online_status === 'no_connection' || (!kobold_world && !force) || (kobold_world_synced && !force)) {
updateWorldStatus();
return;
}
const response = await fetch("/synckoboldworld", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ "name": kobold_world })
});
if (response.ok) {
const syncData = await response.json();
if (syncData.ok) {
kobold_world_synced = true;
kobold_sync_failed = false;
}
if (syncData.busy) {
// console.log('Sync API is busy. Retrying in 3sec');
clearTimeout(timerKoboldSync);
timerKoboldSync = setTimeout(() => syncKoboldWorldInfo(force), 3000);
return;
}
} else {
kobold_sync_failed = true;
let responseLog = response.statusText;
try {
var responseBody = await response.text();
responseLog += ('\n' + responseBody);
} catch {
// empty catch
}
console.error(`Sync API response: ${responseLog}`);
}
updateWorldStatus();
}
$(document).on('input', '#temp', function() {
temp = $(this).val();
@@ -2181,6 +2333,26 @@
koboldai_setting_names = {};
koboldai_setting_names = arr_holder;
// world info settings
koboldai_world_names = data.koboldai_world_names?.length ? data.koboldai_world_names : [];
if(settings.kobold_world != undefined) {
if (koboldai_world_names.includes(settings.kobold_world)) {
kobold_world = settings.kobold_world;
kobold_world_synced = false;
kobold_sync_failed = false;
}
}
koboldai_world_names.forEach((item, i) => {
$('#world_info').append(`<option value='${i}'>${item}</option>`);
// preselect world if saved
if (item == kobold_world){
$('#world_info').val(i).change();
}
});
// end world info settings
preset_settings = settings.preset_settings;
temp = settings.temp;
@@ -2256,7 +2428,6 @@
preset_settings = 'gui';
$("#settings_perset option[value=gui]").attr('selected', 'true');
}
}
//User
@@ -2306,7 +2477,8 @@
model_novel: model_novel,
temp_novel: temp_novel,
rep_pen_novel: rep_pen_novel,
rep_pen_size_novel: rep_pen_size_novel
rep_pen_size_novel: rep_pen_size_novel,
kobold_world: kobold_world,
                 }),
beforeSend: function(){
@@ -2321,7 +2493,8 @@
if(type === 'change_name'){
location.reload();
}
syncKoboldWorldInfo(false);
},
error: function (jqXHR, exception) {
console.log(exception);
@@ -2739,6 +2912,376 @@
$('#load_select_chat_div').css('display', 'block');
});
//**************************WORLD INFO IMPORT EXPORT*************************//
$("#world_import_button" ).click(function() {
$("#world_import_file").click();
});
$("#world_import_file").on("change", function(e) {
var file = e.target.files[0];
if (!file) {
return;
}
const ext = file.name.match(/\.(\w+)$/);
if (!ext || (ext[1].toLowerCase() !== "json")){
return;
}
var formData = new FormData($("#form_world_import").get(0));
jQuery.ajax({
type: 'POST',
url: '/importworldinfo',
data: formData,
beforeSend: () => {},
cache: false,
contentType: false,
processData: false,
success: function(data){
if (data.name) {
imported_world_name = data.name;
updateWorldInfoList(imported_world_name);
}
},
error: (jqXHR, exception) => {},
});
// Will allow to select the same file twice in a row
$('#form_world_import').trigger("reset");
});
async function updateWorldInfoList(importedWorldName) {
var result = await fetch('/getsettings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
if (result.ok) {
var data = await result.json();
koboldai_world_names = data.koboldai_world_names?.length ? data.koboldai_world_names : [];
$('#world_info').find('option[value!="None"]').remove();
koboldai_world_names.forEach((item, i) => {
$('#world_info').append(`<option value='${i}'>${item}</option>`);
});
if (importedWorldName) {
const indexOf = koboldai_world_names.indexOf(kobold_world);
$('#world_info').val(indexOf);
popup_type = 'world_imported';
callPopup('<h3>World imported successfully! Select it now?</h3>');
}
}
}
function download(content, fileName, contentType) {
var a = document.createElement("a");
var file = new Blob([content], {type: contentType});
a.href = URL.createObjectURL(file);
a.download = fileName;
a.click();
}
// World Info Editor
async function showWorldEditor() {
is_world_edit_open = true;
$('#world_popup_name').val(kobold_world);
$('#world_popup').css('display', 'flex');
if (kobold_world) {
const response = await fetch("/getworldinfo", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: kobold_world })
});
if (response.ok) {
kobold_world_data = await response.json();
displayWorldEntries(kobold_world_data);
}
}
}
function hideWorldEditor() {
is_world_edit_open = false;
$('#world_popup').css('display', 'none');
syncKoboldWorldInfo(true);
}
function displayWorldEntries(data) {
$('#world_popup_entries_list').empty();
if (!data || !('entries' in data)) {
return;
}
for (const entryUid in data.entries) {
const entry = data.entries[entryUid];
appendWorldEntry(entry);
}
}
function appendWorldEntry(entry) {
const template = $('#entry_edit_template .world_entry').clone();
template.data('uid', entry.uid);
// key
const keyInput = template.find('input[name="key"]');
keyInput.data('uid', entry.uid);
keyInput.on('input', function () {
const uid = $(this).data('uid');
const value = $(this).val();
kobold_world_data.entries[uid].key = value.split(',').map(x => x.trim()).filter(x => x);
saveWorldInfo();
});
keyInput.val(entry.key.join(',')).trigger('input');
// keysecondary
const keySecondaryInput = template.find('input[name="keysecondary"]');
keySecondaryInput.data('uid', entry.uid);
keySecondaryInput.on('input', function() {
const uid = $(this).data('uid');
const value = $(this).val();
kobold_world_data.entries[uid].keysecondary = value.split(',').map(x => x.trim()).filter(x => x);
saveWorldInfo();
});
keySecondaryInput.val(entry.keysecondary.join(',')).trigger('input');
// comment
const commentInput = template.find('input[name="comment"]');
commentInput.data('uid', entry.uid);
commentInput.on('input', function() {
const uid = $(this).data('uid');
const value = $(this).val();
kobold_world_data.entries[uid].comment = value;
saveWorldInfo();
});
commentInput.val(entry.comment).trigger('input');
// content
const contentInput = template.find('textarea[name="content"]');
contentInput.data('uid', entry.uid);
contentInput.on('input', function() {
const uid = $(this).data('uid');
const value = $(this).val();
kobold_world_data.entries[uid].content = value;
saveWorldInfo();
// count tokens
const numberOfTokens = encode(value).length;
$(this).closest('.world_entry').find('.world_entry_form_token_counter').html(numberOfTokens);
});
contentInput.val(entry.content).trigger('input');
// selective
const selectiveInput = template.find('input[name="selective"]')
selectiveInput.data('uid', entry.uid);
selectiveInput.on('input', function() {
const uid = $(this).data('uid');
const value = $(this).prop('checked');
kobold_world_data.entries[uid].selective = value;
saveWorldInfo();
});
selectiveInput.prop('checked', entry.selective).trigger('input');
selectiveInput.siblings('.checkbox_fancy').click(function() {
$(this).siblings('input').click();
});
// constant
const constantInput = template.find('input[name="constant"]')
constantInput.data('uid', entry.uid);
constantInput.on('input', function() {
const uid = $(this).data('uid');
const value = $(this).prop('checked');
kobold_world_data.entries[uid].constant = value;
saveWorldInfo();
});
constantInput.prop('checked', entry.constant).trigger('input');
constantInput.siblings('.checkbox_fancy').click(function() {
$(this).siblings('input').click();
});
// display uid
template.find('.world_entry_form_uid_value').html(entry.uid);
// delete button
const deleteButton = template.find('input.delete_entry_button');
deleteButton.data('uid', entry.uid);
deleteButton.on('click', function() {
const uid = $(this).data('uid');
deleteWorldInfoEntry(uid);
$(this).closest('.world_entry').remove();
saveWorldInfo();
});
template.appendTo('#world_popup_entries_list');
return template;
}
async function deleteWorldInfoEntry(uid) {
if (!kobold_world_data || !('entries' in kobold_world_data)) {
return;
}
delete kobold_world_data.entries[uid];
if ('folders' in kobold_world_data) {
for (const folderName in kobold_world_data.folders) {
const folder = kobold_world_data.folders[folderName]
const index = folder.indexOf(Number(uid));
if (index !== -1) {
folder.splice(index, 1);
}
}
}
}
function createWorldInfoEntry() {
const newEntryTemplate = {
key: [],
keysecondary: [],
comment: '',
content: '',
constant: false,
selective: false,
};
const newUid = getFreeWorldEntryUid();
if (!Number.isInteger(newUid)) {
console.error("Couldn't assign UID to a new entry");
return;
}
const newEntry = { uid: newUid, ...newEntryTemplate };
kobold_world_data.entries[newUid] = newEntry;
if ('folders' in kobold_world_data) {
if (kobold_world in kobold_world_data.folders && Array.isArray(kobold_world_data.folders)) {
kobold_world_data.folders[kobold_world].push(newUid);
} else {
kobold_world_data.folders[kobold_world] = [newUid];
}
}
const entryTemplate = appendWorldEntry(newEntry);
entryTemplate.get(0).scrollIntoView({behavior: 'smooth'});
}
async function saveWorldInfo(immediately) {
if (!kobold_world || !kobold_world_data) {
return;
}
async function _save() {
const response = await fetch("/editworldinfo", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: kobold_world, data: kobold_world_data })
});
if (response.ok) {
kobold_world_synced = false;
}
}
if (immediately) {
return await _save();
}
clearTimeout(timerWorldSave);
timerWorldSave = setTimeout(async () => await _save(), durationSaveEdit);
}
async function renameWorldInfo() {
const oldName = kobold_world;
const newName = $('#world_popup_name').val();
if (oldName === newName) {
return;
}
kobold_world = newName;
await saveWorldInfo(true);
await deleteWorldInfo(oldName, newName);
}
async function deleteWorldInfo(worldInfoName, selectWorldName) {
if (!koboldai_world_names.includes(worldInfoName)) {
return;
}
const response = await fetch("/deleteworldinfo", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: worldInfoName })
});
if (response.ok) {
await updateWorldInfoList();
const selectedIndex = koboldai_world_names.indexOf(selectWorldName);
if (selectedIndex !== -1) {
$('#world_info').val(selectedIndex).change();
}
else {
$('#world_info').val('None').change();
}
hideWorldEditor();
}
}
function getFreeWorldEntryUid() {
if (!kobold_world_data || !('entries' in kobold_world_data)) {
return null;
}
const MAX_UID = 1_000_000; // <- should be safe enough :)
for (let uid = 0; uid < MAX_UID; uid++) {
if (uid in kobold_world_data.entries) {
continue;
}
return uid;
}
return null;
}
$('#world_info_edit_button').click(() => {
is_world_edit_open ? hideWorldEditor() : showWorldEditor();
});
$('#world_popup_export').click(() => {
if (kobold_world && kobold_world_data) {
const jsonValue = JSON.stringify(kobold_world_data);
const fileName = `${kobold_world}.json`;
download(jsonValue, fileName, 'application/json');
}
});
$('#world_popup_delete').click(() => {
popup_type = 'del_world';
callPopup('<h3>Delete the World Info?</h3>');
});
$('#world_popup_new').click(() => {
createWorldInfoEntry();
});
$('#world_cross').click(() => {
hideWorldEditor();
});
$('#world_popup_name_button').click(() => {
renameWorldInfo();
});
});
    </script>
<title>Tavern.AI</title>
@@ -2796,6 +3339,95 @@
<div></div>
</div>
<div id="world_popup">
<div id="world_popup_text">
<img id="world_cross" src="img/cross.png">
<div id="world_popup_header">
<!-- Consider changing logo to something else -->
<img src="img/book2.png" id="world_logo">
<h3>
World Info Editor
<span>(<a href="/notes/13" target="_blank">?</a>)</span>
</h3>
<div class="world_popup_expander">&nbsp;</div>
<form id="form_rename_world" action="javascript:void(null);" method="post" enctype="multipart/form-data">
<input id="world_popup_name" name="world_popup_name" class="text_pole" maxlength="99" size="32" value="" autocomplete="off">
<input id="world_popup_name_button" type="submit" value="Rename">
</form>
</div>
</div>
<div id="world_popup_entries_list">
</div>
<div id="world_popup_bottom_holder">
<div id="world_popup_new" class="menu_button">New Entry</div>
<div class="world_popup_expander">&nbsp;</div>
<div id="world_popup_export" class="menu_button">Export</div>
<div id="world_popup_delete" class="menu_button">Delete World</div>
</div>
<div id="entry_edit_template">
<div class="world_entry">
<form class="world_entry_form">
<div class="world_entry_form_control">
<label for="key">
<h4>Key</h4>
<h5>Comma-separated list of keywords (e.g: foo,bar).</h5>
</label>
<input class="text_pole" type="text" name="key" placeholder=""/>
</div>
<div class="world_entry_form_control">
<label for="keysecondary">
<h4>Secondary Key</h4>
<h5>Comma-separated list of additional keywords (e.g: foo,bar).</h5>
</label>
<input class="text_pole" type="text" name="keysecondary" placeholder=""/>
</div>
<div class="world_entry_form_control">
<label for="comment">
<h4>Comment</h4>
<h5>Optional comment (doesn't affect the AI).</h5>
</label>
<input class="text_pole" type="text" name="comment" placeholder=""/>
</div>
<div class="world_entry_form_control">
<label for="content">
<h4>Content</h4>
<h5>Text that will be inserted to the prompt upon activation.</h5>
</label>
<textarea class="text_pole" name="content" rows="4" placeholder=""></textarea>
</div>
<div class="world_entry_form_control world_entry_form_horizontal">
<label class="checkbox" for="constant">
<input type="checkbox" name="constant" />
<span class="checkbox_fancy"></span>
<h4>Constant</h4>
</label>
<label class="checkbox" for="selective">
<input type="checkbox" name="selective" />
<span class="checkbox_fancy"></span>
<h4>Selective</h4>
</label>
<span class="world_popup_expander">&nbsp;</span>
<h5 class="world_entry_form_uid">
UID:
&nbsp;
<span class="world_entry_form_uid_value"></span>
</h5>
<h5 class="world_entry_form_tokens">
Tokens used:
&nbsp;
<span class="world_entry_form_token_counter">0</span>
</h5>
<input class="delete_entry_button" type="button" value="Delete Entry" />
</div>
</form>
</div>
</div>
</div>
<div id="shadow_select_chat_popup">
<div id="select_chat_popup">
<div id="select_chat_popup_text">
@@ -2946,6 +3578,25 @@
<h4>Repetition Penalty Range</h4><h5 id="rep_pen_size_counter">select</h5>
<input type="range" id="rep_pen_size" name="volume" min="0" max="2048" step="1">
</div>
<div>
<h4 id="world_info_block">
<span>World Info</span>
<div id="world_import_button" class="right_menu_button"><h2>+Import</h2></div>
</h4>
<h5>How to use (<a href="/notes/13" target="_blank">?</a>)</h5>
<div id="rm_world_import" class="right_menu" style="display: none;">
<form id="form_world_import" action="javascript:void(null);" method="post" enctype="multipart/form-data">
<input type="file" id="world_import_file" accept=".json" name="avatar">
</form>
</div>
<select id="world_info" class="option_select_right_menu">
<option value="None">None</option>
</select>
<input id="world_info_edit_button" type="button" value="Details">
<div id="world_status">
<div id="world_status_indicator"></div><div id="world_status_text"></div>
</div>
</div>
</div>
<div id="novel_api" style="display: none;position: relative;">
<div style="position: absolute; right:152px; top:-25px; opacity:0.25;"><a href="https://novelai.net/" target="_blank"><img src="img/novelai.png" style="width:auto;height:22px;"></a></div>

43
public/notes/13.html Normal file
View File

@@ -0,0 +1,43 @@
<html>
<head>
<title>World Info</title>
<link rel="stylesheet" href="/css/notes.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div id="main">
<div id="content">
<h2>World Info</h2>
<h4>World Info enhances AI's understanding of the details in your world.</h4>
<p>It functions like a dynamic dictionary that only inserts relevant information from World Info entries when keywords associated with the entries are present in the message text.</p>
<p>The KoboldAI engine activates and seamlessly integrates the appropriate lore into the prompt, providing background information to the AI.</p>
<p><i>It is important to note that while World Info helps guide the AI towards your desired lore, it does not guarantee its appearance in the generated output messages.</i></p>
<h3>Pro Tips</h3>
<ul>
<li>The AI does not insert keywords into context, so each World Info entry should be a comprehensive, standalone description.</li>
<li>To create a rich and detailed world lore, entries can be interlinked and reference one another.</li>
<li>To conserve tokens, it is advisable to keep entry contents concise, with a general recommended limit of 50 tokens per entry.</li>
</ul>
<h3>Entry Fields Explained</h3>
<dl>
<dt>Key</dt>
<dd>A list of keywords that trigger the activation of a World Info entry.</dd>
<dt>Secondary Key</dt>
<dd>A list of supplementary keywords that are used in conjunction with the main keywords (see <a href="#Selective">Selective</a>).</dd>
<dt>Content</dt>
<dd>The text that is inserted into the prompt upon entry activation.</dd>
<dt>Comment</dt>
<dd>A supplemental text comment for the your convenience, which is not utilized by the AI.</dd>
<dt>Constant</dt>
<dd>If enabled, the entry would always be present in the prompt. <em>Currently, this is unsupported!</em></dd>
<dt id="Selective">Selective</dt>
<dd>If enabled, the entry would only be inserted when both a Key <b>AND</b> a Secondary Key have been activated. <em>Currently, this is unsupported!</em></dd>
</dl>
</div>
</div>
</body>
</html>

View File

@@ -953,6 +953,234 @@ input[type=button] {
margin-left: 4px;
display: inline-block;
}
#world_info {
margin-bottom: 12px;
margin-right: 4px;
}
#world_info_block {
display: flex;
align-items: center;
}
#world_status{
opacity: 0.5;
margin-top: 2px;
margin-left: 10px;
}
#world_status_indicator{
border-radius: 7px;
width: 14px;
height: 14px;
background-color: red;
display: inline-block;
}
#world_status_text {
margin-left: 4px;
display: inline-block;
}
#world_import_button {
cursor: pointer;
display: inline-block;
padding: 0;
margin: 0;
}
#world_import_button h2 {
margin-top: auto;
margin-bottom: auto;
margin-left: 1rem;
font-size: 16px;
color: rgb(188, 193, 200, 0.5);
}
#world_info_edit_button {
cursor: pointer;
color: #ffffffaa;
padding-left: 0.5rem;
padding-right: 0.5rem;
}
#world_popup {
display: none;
flex-direction: column;
max-width: 800px;
height: 83vh;
position: absolute;
z-index: 2060;
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
margin-top: 0px;
box-shadow: 0 0 2px rgba(200, 200, 200, 0.1);
padding: 4px 36px;
background: #191b31F5;
border-radius: 1px;
}
#world_popup_bottom_holder {
padding: 1rem 0;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
#world_popup_bottom_holder div {
margin-left: 1rem;
cursor: pointer;
user-select: none;
opacity: 0.7;
}
#entry_edit_template {
display: none !important;
}
.world_entry:not(:last-child)::after {
margin-top: 1rem;
height: 1px;
display: block;
width: 100%;
content: '';
background-image: linear-gradient(270deg, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0));
}
#world_popup_header {
display: flex;
flex-direction: row;
align-items: center;
margin-left: 1rem;
}
#form_rename_world {
display: flex;
align-items: center;
margin-right: 20px;
opacity: 0.7;
}
#form_rename_world input[type="submit"] {
cursor: pointer;
}
#form_rename_world input:not(:last-child) {
margin-right: 10px;
}
#world_popup_header h5 {
display: inline-block;
}
.world_popup_expander {
flex-grow: 1;
}
#world_popup_entries_list {
flex-grow: 1;
overflow-y: scroll;
}
#world_popup_entries_list:empty {
width: 100%;
height: 100%;
}
#world_popup_entries_list:empty::before {
content: 'No entries exist. Try creating one!';
font-size: 1.5rem;
font-weight: bolder;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
opacity: 0.7;
}
.world_entry_form_control {
display: flex;
flex-direction: column;
margin: 0 10px;
}
.world_entry_form_control label {
margin-left: 10px;
}
.world_entry_form_control label h4 {
margin-bottom: 0px;
}
.world_entry_form_control label h5 {
margin-top: 3px;
margin-bottom: 3px;
}
.world_entry_form_control textarea {
height: auto;
width: auto;
margin-top: 0;
}
.world_entry_form_control.world_entry_form_horizontal {
flex-direction: row;
align-items: center;
margin-top: 10px;
}
.world_entry_form_control input[type=button] {
opacity: 0.7;
cursor: pointer;
}
.world_entry_form_horizontal h5 {
margin: 0 1rem;
}
.world_entry_form_control .checkbox h4 {
margin-left: 0.5rem;
margin-top: 0;
display: inline-block;
}
.world_entry_form_control .checkbox:not(:first-child) {
margin-left: 2rem;
}
#world_cross {
position: absolute;
right: 15px;
top: 15px;
width: 20px;
height: 20px;
cursor: pointer;
opacity: 0.6;
}
#world_logo {
width: 35px;
height: 35px;
margin-right: 0.5rem;
}
#world_popup h4 a, #world_popup h5 a, #world_popup h3 a {
color: #936f4a;
}
#world_popup h5 a:hover, #world_popup h4 a:hover, #world_popup h4 a:hover a {
color: #998e6b;
}
#world_popup h5 {
color: #757575;
}
.del_checkbox{
display: none;
opacity: 0.5;

397
server.js
View File

@@ -48,7 +48,8 @@ var is_colab = false;
const jsonParser = express.json({limit: '100mb'});
const urlencodedParser = express.urlencoded({extended: true, limit: '100mb'});
const baseRequestArgs = { headers: { "Content-Type": "application/json" } };
const directories = { worlds: 'public/KoboldAI Worlds/' };
app.use(function (req, res, next) { //Security
const clientIp = req.connection.remoteAddress.split(':').pop();
@@ -128,7 +129,7 @@ app.post("/generate", jsonParser, function(request, response_generate = response
    //console.log(request.body.prompt);
//const dataJson = JSON.parse(request.body);
request_promt = request.body.prompt;
//console.log(request.body);
var this_settings = { prompt: request_promt,
use_story:false,
@@ -146,7 +147,7 @@ app.post("/generate", jsonParser, function(request, response_generate = response
use_story:false,
use_memory:false,
use_authors_note:false,
use_world_info:false,
use_world_info:!!request.body.use_world_info,
max_context_length: request.body.max_context_length,
max_length: request.body.max_length,
rep_pen: request.body.rep_pen,
@@ -629,6 +630,12 @@ app.post('/getsettings', jsonParser, (request, response) => { //Wintermute's cod
new Date(fs.statSync(`public/KoboldAI Settings/${a}`).mtime)
);
const worldFiles = fs
.readdirSync(directories.worlds)
.filter(file => path.extname(file).toLowerCase() === '.json')
.sort((a, b) => a < b);
const koboldai_world_names = worldFiles.map(item => path.parse(item).name);
files.forEach(item => {
const file = fs.readFileSync(
`public/KoboldAI Settings/${item}`,
@@ -671,11 +678,64 @@ app.post('/getsettings', jsonParser, (request, response) => { //Wintermute's cod
settings,
koboldai_settings,
koboldai_setting_names,
koboldai_world_names,
novelai_settings,
novelai_setting_names
});
});
// Work around to disable parallel requests to endpoint
let kobold_world_sync_busy = false;
app.post('/synckoboldworld', jsonParser, async (request, response) => {
if(!request.body) return response.sendStatus(400);
if (!api_server || kobold_world_sync_busy) {
response.send({ busy: true });
return;
}
try {
kobold_world_sync_busy = true;
const worldName = request.body.name;
await synchronizeKoboldWorldInfo(worldName);
response.send({ ok: true });
} catch (err) {
var message = JSON.stringify(err);
console.error(`Error during world synchronization: ${message}`);
response.status(500).send(message);
} finally {
kobold_world_sync_busy = false;
}
});
app.post('/getworldinfo', jsonParser, async (request, response) => {
if (!request.body?.name) {
return response.sendStatus(400);
}
const file = readWorldInfoFile(request.body.name);
return response.send(file.tavernWorldInfo);
});
app.post('/deleteworldinfo', jsonParser, async (request, response) => {
if (!request.body?.name) {
return response.sendStatus(400);
}
const worldInfoName = request.body.name;
const filename = `${worldInfoName}.json`;
const pathToWorldInfo = path.join(directories.worlds, filename);
if (!fs.existsSync(pathToWorldInfo)) {
throw new Error(`World info file ${filename} doesn't exist.`);
}
fs.rmSync(pathToWorldInfo);
return response.sendStatus(200);
});
function getCharaterFile(directories,response,i){ //old need del
if(directories.length > i){
@@ -1038,10 +1098,341 @@ app.post("/importchat", urlencodedParser, function(request, response){
});
app.post('/importworldinfo', urlencodedParser, (request, response) => {
if(!request.file) return response.sendStatus(400);
const filename = request.file.originalname;
if (path.parse(filename).ext.toLowerCase() !== '.json') {
return response.status(400).send('Only JSON files are supported.')
}
const pathToUpload = path.join('./uploads/' + request.file.filename);
const fileContents = fs.readFileSync(pathToUpload, 'utf8');
try {
const worldContent = JSON.parse(fileContents);
if (!('entries' in worldContent)) {
throw new Error('File must contain a world info entries list');
}
} catch (err) {
return response.status(400).send('Is not a valid world info file');
}
const pathToNewFile = path.join(directories.worlds, filename);
const worldName = path.parse(pathToNewFile).name;
if (!worldName) {
return response.status(400).send('World file must have a name');
}
fs.writeFileSync(pathToNewFile, fileContents);
return response.send({ name: worldName });
});
app.post('/editworldinfo', jsonParser, (request, response) => {
if (!request.body) {
return response.sendStatus(400);
}
if (!request.body.name) {
return response.status(400).send('World file must have a name');
}
try {
if (!('entries' in request.body.data)) {
throw new Error('World info must contain an entries list');
}
} catch (err) {
return response.status(400).send('Is not a valid world info file');
}
const filename = `${request.body.name}.json`;
const pathToFile = path.join(directories.worlds, filename);
fs.writeFileSync(pathToFile, JSON.stringify(request.body.data));
return response.send({ ok: true });
});
function findTavernWorldEntry(info, key, content) {
for (const entryId in info.entries) {
const entry = info.entries[entryId];
const keyString = entry.key.join(',');
if (keyString === key && entry.content === content) {
return entry;
}
}
return null;
}
async function synchronizeKoboldWorldInfo(worldInfoName) {
const { koboldFolderName, tavernWorldInfo } = readWorldInfoFile(worldInfoName);
// Get existing world info
const koboldWorldInfo = await getAsync(`${api_server}/v1/world_info`, baseRequestArgs);
// Validate kobold world info
let {
shouldCreateWorld,
koboldWorldUid,
tavernEntriesToCreate,
koboldEntriesToDelete,
koboldFoldersToDelete,
} = await validateKoboldWorldInfo(koboldFolderName, koboldWorldInfo, tavernWorldInfo);
// Create folder if not already exists
if (koboldFolderName && shouldCreateWorld) {
koboldWorldUid = await createKoboldFolder(koboldFolderName, tavernEntriesToCreate, tavernWorldInfo);
}
await deleteKoboldFolders(koboldFoldersToDelete);
await deleteKoboldEntries(koboldEntriesToDelete);
await createTavernEntries(tavernEntriesToCreate, koboldWorldUid, tavernWorldInfo);
}
function readWorldInfoFile(worldInfoName) {
if (!worldInfoName) {
return { koboldFolderName: null, tavernWorldInfo: { entries: {}, folders: {} }};
}
const koboldFolderName = getKoboldWorldInfoName(worldInfoName);
const filename = `${worldInfoName}.json`;
const pathToWorldInfo = path.join(directories.worlds, filename);
if (!fs.existsSync(pathToWorldInfo)) {
throw new Error(`World info file ${filename} doesn't exist.`);
}
const tavernWorldInfoText = fs.readFileSync(pathToWorldInfo, 'utf8');
const tavernWorldInfo = JSON.parse(tavernWorldInfoText);
return { koboldFolderName, tavernWorldInfo };
}
async function createKoboldFolder(koboldFolderName, tavernEntriesToCreate, tavernWorldInfo) {
const createdFolder = await postAsync(`${api_server}/v1/world_info/folders`, { data: {}, ...baseRequestArgs });
const koboldWorldUid = createdFolder.uid;
// Set a name so we could find the folder later
const setNameArgs = { data: { value: koboldFolderName }, ...baseRequestArgs };
await putAsync(`${api_server}/v1/world_info/folders/${koboldWorldUid}/name`, setNameArgs);
// Create all world info entries
tavernEntriesToCreate.push(...Object.keys(tavernWorldInfo.entries));
return koboldWorldUid;
}
async function createTavernEntries(tavernEntriesToCreate, koboldWorldUid, tavernWorldInfo) {
if (tavernEntriesToCreate.length && koboldWorldUid) {
for (const tavernUid of tavernEntriesToCreate) {
try {
const tavernEntry = tavernWorldInfo.entries[tavernUid];
const koboldEntry = await postAsync(`${api_server}/v1/world_info/folders/${koboldWorldUid}`, { data: {}, ...baseRequestArgs });
await setKoboldEntryData(tavernEntry, koboldEntry);
} catch (err) {
console.error(`Couldn't create Kobold world info entry, tavernUid=${tavernUid}. Skipping...`);
console.error(err);
}
}
}
}
async function deleteKoboldEntries(koboldEntriesToDelete) {
if (koboldEntriesToDelete.length) {
for (const uid of koboldEntriesToDelete) {
try {
await deleteAsync(`${api_server}/v1/world_info/${uid}`);
} catch (err) {
console.error(`Couldn't delete Kobold world info entry, uid=${uid}. Skipping...`);
console.error(err);
}
}
}
}
async function deleteKoboldFolders(koboldFoldersToDelete) {
if (koboldFoldersToDelete.length) {
for (const uid of koboldFoldersToDelete) {
try {
await deleteAsync(api_server + `/v1/world_info/folders/${uid}`, baseRequestArgs);
} catch (err) {
console.error(`Couldn't delete Kobold world info folder, uid=${uid}. Skipping...`);
console.error(err);
}
}
}
}
async function setKoboldEntryData(tavernEntry, koboldEntry) {
// 1. Set primary key
if (tavernEntry.key?.length) {
const keyArgs = { data: { value: tavernEntry.key.join(',') }, ...baseRequestArgs };
await putAsync(`${api_server}/v1/world_info/${koboldEntry.uid}/key`, keyArgs);
}
// 2. Set secondary key
if (tavernEntry.keysecondary?.length) {
const keySecondaryArgs = { data: { value: tavernEntry.keysecondary.join(',') }, ...baseRequestArgs };
await putAsync(`${api_server}/v1/world_info/${koboldEntry.uid}/keysecondary`, keySecondaryArgs);
}
// 3. Set content
if (tavernEntry.content) {
const contentArgs = { data: { value: tavernEntry.content }, ...baseRequestArgs };
await putAsync(`${api_server}/v1/world_info/${koboldEntry.uid}/content`, contentArgs);
}
// 4. Set comment
if (tavernEntry.comment) {
const commentArgs = { data: { value: tavernEntry.comment }, ...baseRequestArgs };
await putAsync(`${api_server}/v1/world_info/${koboldEntry.uid}/comment`, commentArgs);
};
/* Can't set these via API due to bug in Kobold)
// 5. Set constant flag
if (tavernEntry.constant) {
const constantArgs = { data: { value: tavernEntry.constant.toString() }, ...baseRequestArgs };
await putToPromise(`${api_server}/v1/world_info/${koboldEntry.uid}/constant`, constantArgs);
}
// 6. Set selective flag
if (tavernEntry.selective) {
const selectiveArgs = { data: { value: tavernEntry.selective.toString() }, ...baseRequestArgs };
await putToPromise(`${api_server}/v1/world_info/${koboldEntry.uid}/selective`, selectiveArgs);
}
*/
}
async function validateKoboldWorldInfo(koboldFolderName, koboldWorldInfo, tavernWorldInfo) {
let shouldCreateWorld = true;
let koboldWorldUid = null;
const koboldEntriesToDelete = []; // KoboldUIDs
const koboldFoldersToDelete = []; // KoboldUIDs
const tavernEntriesToCreate = []; // TavernUIDs
if (koboldWorldInfo?.folders?.length) {
let existingFolderAlreadyFound = false;
for (const folder of koboldWorldInfo.folders) {
// Don't care about non-Tavern folders
if (!isTavernKoboldWorldInfo(folder.name)) {
continue;
}
// Other Tavern folders should be deleted (including dupes). If folder name selected is null, then delete anyway to clean-up
if (!koboldFolderName || folder.name !== koboldFolderName || existingFolderAlreadyFound) {
koboldFoldersToDelete.push(folder.uid);
// Should also delete all entries in folder otherwise they will be detached
if (Array.isArray(folder.entries)) {
koboldEntriesToDelete.push(...folder.entries.map(entry => entry.uid));
}
}
// Validate existing entries in Kobold world
if (folder.name === koboldFolderName) {
existingFolderAlreadyFound = true;
shouldCreateWorld = false;
koboldWorldUid = folder.uid;
if (folder.entries?.length) {
const foundTavernEntries = [];
for (const koboldEntry of folder.entries) {
const tavernEntry = findTavernWorldEntry(tavernWorldInfo, koboldEntry.key, koboldEntry.content);
if (tavernEntry) {
foundTavernEntries.push(tavernEntry.uid);
if (isEntryOutOfSync(tavernEntry, koboldEntry)) {
// Entry is out of sync. Should be recreated
koboldEntriesToDelete.push(koboldEntry.uid);
tavernEntriesToCreate.push(tavernEntry.uid);
}
}
else {
// We don't have that entry in our world. It should be deleted
koboldEntriesToDelete.push(koboldEntry.uid);
}
}
// Check if every tavern entry was found in kobold world
// BTW. Entries is an object, not an array!
for (const tavernEntryUid in tavernWorldInfo.entries) {
const tavernEntry = tavernWorldInfo.entries[tavernEntryUid];
if (!foundTavernEntries.includes(tavernEntry.uid)) {
tavernEntriesToCreate.push(tavernEntry.uid);
}
}
}
}
}
}
return { shouldCreateWorld, koboldWorldUid, tavernEntriesToCreate, koboldEntriesToDelete, koboldFoldersToDelete };
}
function isEntryOutOfSync(tavernEntry, koboldEntry) {
return tavernEntry.content !== koboldEntry.content ||
tavernEntry.comment !== koboldEntry.comment ||
tavernEntry.selective !== koboldEntry.selective ||
tavernEntry.constant !== koboldEntry.constant ||
tavernEntry.key.join(',') !== koboldEntry.key ||
tavernEntry.keysecondary.join(',') !== koboldEntry.keysecondary;
}
// ** REST CLIENT ASYNC WRAPPERS **
function deleteAsync(url, args) {
return new Promise((resolve, reject) => {
client.delete(url, args, (data, response) => {
if (response.statusCode >= 400) {
reject(data);
}
resolve(data);
}).on('error', e => reject(e));
})
}
function putAsync(url, args) {
return new Promise((resolve, reject) => {
client.put(url, args, (data, response) => {
if (response.statusCode >= 400) {
reject(data);
}
resolve(data);
}).on('error', e => reject(e));
})
}
function postAsync(url, args) {
return new Promise((resolve, reject) => {
client.post(url, args, (data, response) => {
if (response.statusCode >= 400) {
reject(data);
}
resolve(data);
}).on('error', e => reject(e));
})
}
function getAsync(url, args) {
return new Promise((resolve, reject) => {
client.get(url, args, (data, response) => {
if (response.statusCode >= 400) {
reject(data);
}
resolve(data);
}).on('error', e => reject(e));
})
}
// ** END **
function getKoboldWorldInfoName(worldInfoName) {
return worldInfoName ? `TavernAI_${worldInfoName}_WI` : null;
}
function isTavernKoboldWorldInfo(folderName) {
return /^TavernAI_(.*)_WI$/.test(folderName);
}
app.listen(server_port, function() {