Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- ////////////// Shared Library
- //https://stackoverflow.com/questions/273789/is-there-a-version-of-javascripts-string-indexof-that-allows-for-regular-expr#273810
- String.prototype.regexLastIndexOf = function (regex, startpos) { regex = (regex.global) ? regex : new RegExp(regex.source, "g" + (regex.ignoreCase ? "i" : "") + (regex.multiLine ? "m" : "")); if (typeof (startpos) == "undefined") { startpos = this.length; } else if (startpos < 0) { startpos = 0; } let stringToWorkWith = this.substring(0, startpos + 1); let lastIndexOf = -1; let nextStop = 0; while ((result = regex.exec(stringToWorkWith)) != null) { lastIndexOf = result.index; regex.lastIndex = ++nextStop; } return lastIndexOf; }
- function shuffleArray(array) {
- for (var i = array.length - 1; i > 0; i--) {
- var j = Math.floor(Math.random() * (i + 1));
- var temp = array[i];
- array[i] = array[j];
- array[j] = temp;
- }
- return array;
- }
- const calcLine = (list,ind) => {
- ind = Math.abs(ind);
- for (var i =1; i <= Math.min(list.length,ind); i++){
- console.log("CalcLine: ("+i+") "+list[list.length-i])
- if(list[list.length-i] == ""){
- ind++;
- }
- }
- return ind;
- }
- const getHeader = (type) => {
- for(var i = 0 ; i < worldEntries.length;i++){
- if(worldEntries[i]["keys"].match(/(?<=h=)\d+/) == type){
- return "\n==\n"+worldEntries[i]["entry"]+"\n";
- }
- }
- return "";
- }
- const deformatKeys = (text) =>{
- text = text.replace(/ *\[[^)]*\] */g, "");
- text = text.replace(/\$/g,"");
- text = text.replace(/\|/g,",");
- text = text.replace(/,(?=[^\s])/g, ", ");
- if(text.endsWith(", ")){text = text.slice(0,-2)}
- if(text.endsWith(",")){text = text.slice(0,-1)}
- return text;
- }
- const reformatKeys = (text, type) =>{
- text = "$"+text.replace(/, /g,", $");
- text+=", [t="+type+"]";
- return text;
- }
- const getRawHistoryString = (chars) => history.map(element => element["text"]).join(' ').slice(chars);
- const getHistoryString = (turns) => history.slice(turns).map(element => element["text"]).join(' ') // Returns a single string of the text.
- const getHistoryText = (turns) => history.slice(turns).map(element => element["text"]) // Returns an array of text.
- const getActionTypes = (turns) => history.slice(turns).map(element => element["type"]) // Returns the action types of the previous turns in an array.
- const hasAttributes = (keys) => {const attrib = keys.match(/([a-z](=\d+)?)/g); if(attrib) {return attrib.map(attrib => attrib.split('='))}} // Pass it a bracket-encapsulated string and it returns an array of [attribute, value] pairs if possible.
- const everyCheck = (entry, string) => // an AND/OR check for keywords. Not foolproof, but should be fine as long as proper syntax is abided.
- {
- string = string.toLowerCase().trim()
- const keys = entry["keys"].replace(/\$/g, '').replace(/\[(.+)?\]/g, '').toLowerCase().trim().split(',')
- let anyArray = keys.filter(element => element.includes('|'))
- const anyCheck = anyArray.every(element => element.split('|').some(key => string.includes(key.trim())))
- const everyArray = keys.filter(element => !element.includes('|')).every(key => string.includes(key.trim()))
- console.log(`Keys: ${keys}, Any: ${anyArray}, Every: ${everyArray}`)
- if (everyArray && anyCheck) {
- return true
- }
- }
- String.prototype.extractString = function(a, b) {return this.slice(this.indexOf(a), this.indexOf(b) +1)} // Slightly cleaner to read and write than doing the indexing yourself.
- const getTypeEntries = (type) => {const t = type; return worldEntries.filter(a => a["keys"].match(/(?<=t=)\d+/) == t) }
- const spliceLines = (string, pos, req = 1) => { if (lines.length > req) {lines.splice(pos, 0, string.replace(/[\{\}]/g,""))}} // This is run on each of the additional context to position them accordingly.
- const spliceMiddle = (array, pos, req = 1) => {
- array = array.sort((a,b) => {return a.value - b.value})
- if(array.map(x => x.text).join("").length > 700){
- do{
- contextStacks["backMemory"][0].push({text:array[0].text,value:array[0].value});
- array.shift();
- }while(array.map(x => x.text).join("").length > 700)
- }
- lines.splice(lines.length-calcLine(lines,pos), 0, array.map(x => x.text).join("").replace(/[\{\}]/g,""))
- }
- //const spliceMemory = (string, pos, req = 1) => contextMemory.splice(pos, 0, string.replace(/[\{\}]/g,"")+"\n==\n")
- const spliceMemory = (array, pos, req = 1) => {
- array = array.sort((a,b) => {return a.value - b.value})
- console.log(JSON.stringify(array));
- if(array.map(x=>x.text).join("").length > 600){
- do{array.shift();}
- while(array.map(x=>x.text).join("").length > 600)
- }
- console.log("post clean");
- console.log(JSON.stringify(array));
- contextMemory.splice(pos,0,array.map(x=>x.text).join("").replace(/[\{\}]/g,"")+"\n==\n")
- }
- const getMemory = (text) => {return info.memoryLength ? text.slice(0, info.memoryLength - 1) : ''} // If memoryLength is set then slice of the beginning until the end of memoryLength, else return an empty string.
- const getContext = (text) => {return info.memoryLength ? text.slice(info.memoryLength).replace(/^[^A-Z].+[.]/, '') : text} // If memoryLength is set then slice from the end of memory to the end of text, else return the entire text.
- // This basically took a 'just make it work 4Head approach', not happy with it, but it *does* work.
- // The "challenge" is to insert the words on their last appearance rather than simply inserting on linebreaks.
- // Whole-word match should be used, but I'm not keen on re-iterating the same searches in alernative means several times.
- // Adding parenthesized descriptors should be done indepentendly of the 'lastInputString' search, but that'd somewhat conflict with the current attribute system (I'll look into it).
- // The issue with having a ton of semi- persistent parenthesized enapsulations is that the AI will begin using them too (consider doing last mention before previous action?)
- // Though with the intended use- case (short descriptors) it shouldn't be too much of an issue e.g John (the hero) or David (party member); this is one of the reason why I've placed a limiter of utilizing the first sentence only.
- const addDescription = (entry, value = 0, ind = 0) =>
- {
- const searchKeys = entry["keys"].replace(/\$/g, '').replace(/\[(.+)?\]/g, '').replace(/\|/g, ',').split(',').filter(element => element.trim().length >= 1)
- let finalIndex = null;
- let keyPhrase = null;
- searchKeys.forEach(key => { if(!assignedDescriptors.includes(key)) {const regEx = new RegExp(`\\b${key.trim()}\\b`,"gi"); const keyIndex = context.toLowerCase().regexLastIndexOf(regEx); if (keyIndex > finalIndex) {finalIndex = keyIndex; keyPhrase = key; assignedDescriptors.push(key)}}}) // Find the last mention of a valid key from the entry.
- if (finalIndex)
- {
- console.log(finalIndex)
- const beginString = context.slice(0, finalIndex) // Establish the point up until the phrase/word we want to append to.
- const endString = context.slice(finalIndex + keyPhrase.length)
- //const entryString = entry["entry"].match(/[^.!?]+/) // Extract the first sentence from the entry - unsure about whether long descriptors should be permitted, and also how they would affect the generation flow.
- const entryString = entry["entry"].split('{')[1].split('}')[0];
- context = beginString + keyPhrase + ' (' + entryString + ')' + endString; // Assemble it with parenthesis encapsulation (less likely to interrupt narrative).
- }
- lines = context.split('\n')
- }
- const addAuthorsNote = (entry, value = 0, ind = 0) => state.memory.authorsNote = `${entry["entry"]}`
- const revealWorldEntry = (entry, value = 0, ind = 0) => entry.isNotHidden = true
- // frontMemory is fairly dependant on cooldowns which is why it's the only one that has that implemented at the moment.
- const addFrontMemory = (entry, value = 0, ind = 0) =>
- {
- if (!entry.fLastSeen) {entry.fLastSeen = info.actionCount};
- if (info.actionCount - entry.fLastSeen >= value) {entry.fLastSeen = info.actionCount;}
- if ((info.actionCount % entry.fLastSeen) == value || (info.actionCount - entry.fLastSeen == 0))
- {
- contextStacks["frontMemory"][0] = contextStacks["frontMemory"][0].replace(/\n\n>/gm, '').trim();
- contextStacks["frontMemory"][0] += `\n> ${entry["entry"]}`;
- entry.fLastSeen = info.actionCount;
- }
- }
- const addBackMemory = (entry, value = 0, ind = 0) => {contextStacks["backMemory"][0].push({text: "["+entry["entry"]+"]\n", value:ind});}
- const addMiddleMemory = (entry, value = 0, ind = 0) => {
- contextStacks["middleMemory"][0].push({text: "["+entry["entry"]+"]\n", value: ind});
- }
- /////////////// Input Modifier
- // Checkout the repo examples to get an idea of other ways you can use scripting
- // https://github.com/AIDungeon/Scripting/blob/master/examples
- const modifier = (text) => {
- let modifiedText = text
- const lowered = text.toLowerCase()
- state.message = null;
- state.entryGen = [];
- state.generate = false;
- state.memory.frontMemory = "";
- state.break = false;
- var args = modifiedText.match(/\[([^\]]+)\]/);
- var extraTags = lowered.replace(args,"").replace("/wi")
- args = args ? args[1] : [];
- if(lowered.includes("/sort")){
- worldEntries = worldEntries.sort((a,b)=>{return a.entry.toLowerCase() > b.entry.toLowerCase() ? 1 : -1})
- //state.break = true;
- return{text:"",stop:true}
- }
- if(lowered.includes("/wi") && args.length > 0){
- args = args.split("|");
- state.entryGen = args;
- state.generate = true;
- state.newEntry = {keys:"",entry:""};
- modifiedText = "";
- var type = args[args.length-1];
- var chosenEntries = getTypeEntries(type);
- var string = "";
- if(chosenEntries.length ==0){state.message = "Error: No entries of type found"}else{
- chosenEntries = shuffleArray(chosenEntries);
- string+=getHeader(type);
- for(var i =0 ; i < Math.min(chosenEntries.length,5);i++){
- string+="{(Tags: "+deformatKeys(chosenEntries[i]["keys"])+") "+chosenEntries[i]["entry"].replace(/[\{\}]/g,"")+"}\n"
- }
- string+="{(Tags:";
- if(args.length >=2){string+=" "+args[0]+")"; state.newEntry.keys = reformatKeys(args[0],type)}
- if(args.length >=3){string+=" "+args[1];state.newEntry.entry = args[1];}
- }
- state.memory.frontMemory = string;
- }
- // You must return an object with the text property defined.
- return { text: modifiedText }
- }
- // Don't modify this part
- modifier(text)
- ////////////// Context Modifier
- let contextMemory = getMemory(text).split('\n').filter(function (el) {
- return el != "";
- });
- let context = getContext(text);
- let lines = context.split('\n'); // Split the line-breaks within the context into separated strings in an array.
- const entryFunctions = {
- 'a': addAuthorsNote, // [a] adds it as authorsNote, only one authorsNote at a time.
- 'f': addFrontMemory, // [f] adds it to the frontMemory stack, multiple can be added at a time, but only the latest one becomes an action.
- 'r': revealWorldEntry, // [r] reveals the entry once mentioned, used in conjuction with [e] to only reveal if all keywords are mentioned at once.
- 'c': addBackMemory, // [c] adds it to context, recommended to pre-fix keywords with $ to avoid duplicates from normal processing.
- 'e': () => {}, // [e] tells the custom keyword check to only run the above functions if every keyword of the entry matches.
- 'd': addDescription, // [d] adds the first sentence of the entry as a short, parenthesized descriptor to the last mention of the revelant keyword(s) e.g John (a business man)
- 'm': addMiddleMemory, // [i] adds a stack of entries two lines above the last input, serves as a mix between authorsNote and frontMemory.
- 'w': () => {}, // [w] assigns the weight attribute, the higher value the more recent/relevant it will be in context/frontMemory/intermediateMemory etc.
- 't': () => {}, // [t] assigns a "type" value to the entry, used for random world info entries.
- 'h': () => {}, // [h] assigns a "type header" value to the entry, to classify world info types.
- }
- // To avoid complicating it with measurements of the additonal string, and at the cost of slightly less flexibility, we assign different functions to handle the positioning.
- // spliceMemory would be to position it 'at the top of context'/'end of memory' while spliceLines is for short/medium injections towards the lower part of context.
- let contextStacks = { // Handle the positioning of the various additional contexts in a formated list to have an easier overview of what's going on.
- 'frontMemory': ["", lines.length, spliceLines], // Splice it at the end of the array, push would naturally be easier, but /shrug
- 'middleMemory': [[], 3, spliceMiddle], // Splice it one line back.
- 'backMemory': [[], contextMemory.length, spliceMemory] // Splice it at the end of memory contextMemory.length
- }
- let assignedDescriptors = [] // Assemble a list of descriptors that have already been assigned (in an attempt) to avoid duplicates.
- // Pass the worldEntries list and check attributes, then process them.
- const processWorldEntries = (entries) =>
- {
- entries = [...entries] // Copy the entries to avoid in-place manipulation.
- const lastTurnString = getRawHistoryString(-900).toLowerCase().trim() // What we check the keywords against, this time around we basically check where in the context the last history element is then slice forward.
- entries = shuffleArray(entries);
- entries.sort((a, b) => a["keys"].match(/(?<=w=)\d+/) - b["keys"].match(/(?<=w=)\d+/)).forEach(wEntry => // Take a quick sprint through the worldEntries list and process its elements.
- {
- var lastIndex = -1;
- wEntry["keys"].replace(/\$/g, '').replace(/\|/g, ',').split(',').forEach(keyword => {
- lastIndex = Math.max(lastIndex,lastTurnString.lastIndexOf(keyword.toLowerCase().trim()))
- })
- console.log(`Entry: ${wEntry["keys"]}: LastIndex: ${lastIndex}`)// Only process attributes of entries detected on the previous turn. (Using the presumed native functionality of substring acceptance instead of RegEx wholeword match)
- // During the custom check we also (temporarily) remove the '$' prefix as to not need special processing of that later, a trim is also done.
- if (lastIndex >= 0)
- {
- var used = false;
- try // We try to do something. If code goes kaboom then we just catch the error and proceed. This is to deal with non-attribute assigned entries e.g those with empty bracket-encapsulations []
- {
- // Get the attribute value pairs. [attrib, value]
- const entryAttributes = hasAttributes(wEntry["keys"].extractString('[', ']'))
- // Do a strict/every match if it's flagged as such, entry will only be processed if all keywords match as opposed to any.
- if (entryAttributes.some(attrib => attrib.includes('e')))
- { if (everyCheck(wEntry, lastTurnString)) { entryAttributes.forEach(attrib => entryFunctions[attrib[0]](wEntry, attrib[1], lastIndex)); used = true; } }
- // If it's not flagged with 'e' then process as normal (any check)
- else {entryAttributes.forEach(attrib => entryFunctions[attrib[0]](wEntry, attrib[1], lastIndex)); used = true;}
- }
- catch (error) {console.log(error)} // Catch the error as it'd most likely not be destructive or otherwise detrimental.
- }
- })
- }
- const modifier = (text) =>
- {
- state.memory.authorsNote = "";
- if(!state.generate){
- var trailingLine = false;
- if(text.slice(-1)=="\n"){trailingLine = true;}
- // Position the various attribute tags, push them into temporary lists etc.
- if (worldEntries) {processWorldEntries(worldEntries);}
- // Position the various additional context content into their correct positions.
- Object.keys(contextStacks).forEach(key => {contextStacks[key][2](contextStacks[key][0], contextStacks[key][1])})
- contextMemory = contextMemory.join('\n')
- const combinedLines = lines.join("\n").slice(-(info.maxChars - contextMemory.length)) // Account for additional context added to memory
- const finalText = [contextMemory, (combinedLines.slice(-1) == "\n" && trailingLine == false ? combinedLines.slice(0,-1) : combinedLines)].join("")
- // return {text: finalText.replace(/^[^A-Z].+[,;.:]/, '')}
- return{text:finalText};
- }else{
- if(state.memory.frontMemory.length > 500){
- text = text.slice(0,-500)+state.memory.frontMemory.slice(0,-500)+text.slice(-500);
- }
- return {text:text};
- }
- }
- modifier(text)
- ///////////////// Output Modifier
- // To prevent the AI from outputting parenthesized information, we simply remove it.
- const modifier = (text) =>
- {
- state.memory.frontMemory = "";
- let modifiedText = text;
- if(state.break){state.break = false;return {text:""}}
- if(!state.generate){
- modifiedText = modifiedText.replace(/ ?\(.*\)/gm, '');
- state.message = "";
- return {text: modifiedText}
- }
- else{
- console.log("Current output: "+modifiedText);
- modifiedText = modifiedText.split("}")[0];
- if(state.newEntry.keys==""){
- modifiedText = modifiedText.split(")");
- state.newEntry.keys = reformatKeys(modifiedText[0],state.entryGen[state.entryGen.length-1]);
- modifiedText.shift()
- state.newEntry.entry = modifiedText.join(")");}
- else{
- state.newEntry.entry += modifiedText;
- }
- state.message = "New entry created: {(Tags: "+state.newEntry.keys+") "+state.newEntry.entry;
- state.newEntry.isNotHidden = true;
- worldEntries.push(state.newEntry);
- state.newEntry = {};
- state.generate = false;
- return{text:""}
- }
- }
- modifier(text)
Add Comment
Please, Sign In to add comment