diff --git a/public/notes/13.html b/public/notes/13.html
new file mode 100644
index 000000000..c15c35341
--- /dev/null
+++ b/public/notes/13.html
@@ -0,0 +1,43 @@
+
+
+
World Info
+
+
+
+
+
+
+
+
World Info
+
World Info enhances AI's understanding of the details in your world.
+
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.
+
The KoboldAI engine activates and seamlessly integrates the appropriate lore into the prompt, providing background information to the AI.
+
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.
+
+
Pro Tips
+
+ The AI does not insert keywords into context, so each World Info entry should be a comprehensive, standalone description.
+ To create a rich and detailed world lore, entries can be interlinked and reference one another.
+ To conserve tokens, it is advisable to keep entry contents concise, with a general recommended limit of 50 tokens per entry.
+
+
+
Entry Fields Explained
+
+ Key
+ A list of keywords that trigger the activation of a World Info entry.
+ Secondary Key
+ A list of supplementary keywords that are used in conjunction with the main keywords (see Selective ).
+ Content
+ The text that is inserted into the prompt upon entry activation.
+ Comment
+ A supplemental text comment for the your convenience, which is not utilized by the AI.
+ Constant
+ If enabled, the entry would always be present in the prompt. Currently, this is unsupported!
+ Selective
+ If enabled, the entry would only be inserted when both a Key AND a Secondary Key have been activated. Currently, this is unsupported!
+
+
+
+
+
+
diff --git a/public/style.css b/public/style.css
index 7db39645b..0d16b4257 100644
--- a/public/style.css
+++ b/public/style.css
@@ -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;
diff --git a/server.js b/server.js
index 1156d1929..0776bbb76 100644
--- a/server.js
+++ b/server.js
@@ -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() {