Merge pull request #2857 from SillyTavern/wi-delay-recursion-levels

WI "Delay until recursion" levels to delay delayed entries until other delayed entries are fully matched
This commit is contained in:
Cohee 2024-09-17 01:00:03 +03:00 committed by GitHub
commit 0b0bd27321
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 91 additions and 18 deletions

View File

@ -274,3 +274,7 @@ select.keyselect+span.select2-container .select2-selection--multiple {
align-self: center; align-self: center;
width: 100%; width: 100%;
} }
.world_entry:has(input[name="delay_until_recursion"]:not(:checked)) .world_entry_form_control:has(input[name="delayUntilRecursionLevel"]) {
display: none;
}

View File

@ -5473,23 +5473,23 @@
<div class="world_entry_form_control flex1"> <div class="world_entry_form_control flex1">
<small class="textAlignCenter" data-i18n="Case-Sensitive">Case-Sensitive</small> <small class="textAlignCenter" data-i18n="Case-Sensitive">Case-Sensitive</small>
<select name="caseSensitive" class="text_pole widthNatural margin0"> <select name="caseSensitive" class="text_pole widthNatural margin0">
<option value="null" data-i18n="Use global setting">Use global setting</option> <option value="null" data-i18n="Use global">Use global</option>
<option value="true" data-i18n="Yes">Yes</option> <option value="true" data-i18n="Yes">Yes</option>
<option value="false" data-i18n="No">No</option> <option value="false" data-i18n="No">No</option>
</select> </select>
</div> </div>
<div class="world_entry_form_control flex1"> <div class="world_entry_form_control flex1">
<small class="textAlignCenter" data-i18n="Match Whole Words">Match Whole Words</small> <small class="textAlignCenter" data-i18n="Whole Words">Whole Words</small>
<select name="matchWholeWords" class="text_pole widthNatural margin0"> <select name="matchWholeWords" class="text_pole widthNatural margin0">
<option value="null" data-i18n="Use global setting">Use global setting</option> <option value="null" data-i18n="Use global">Use global</option>
<option value="true" data-i18n="Yes">Yes</option> <option value="true" data-i18n="Yes">Yes</option>
<option value="false" data-i18n="No">No</option> <option value="false" data-i18n="No">No</option>
</select> </select>
</div> </div>
<div class="world_entry_form_control flex1"> <div class="world_entry_form_control flex1">
<small class="textAlignCenter" data-i18n="Use Group Scoring">Use Group Scoring</small> <small class="textAlignCenter" data-i18n="Group Scoring">Group Scoring</small>
<select name="useGroupScoring" class="text_pole widthNatural margin0"> <select name="useGroupScoring" class="text_pole widthNatural margin0">
<option value="null" data-i18n="Use global setting">Use global setting</option> <option value="null" data-i18n="Use global">Use global</option>
<option value="true" data-i18n="Yes">Yes</option> <option value="true" data-i18n="Yes">Yes</option>
<option value="false" data-i18n="No">No</option> <option value="false" data-i18n="No">No</option>
</select> </select>
@ -5498,6 +5498,13 @@
<small class="textAlignCenter" data-i18n="Automation ID">Automation ID</small> <small class="textAlignCenter" data-i18n="Automation ID">Automation ID</small>
<input class="text_pole margin0" name="automationId" type="text" placeholder="( None )" data-i18n="[placeholder]( None )"> <input class="text_pole margin0" name="automationId" type="text" placeholder="( None )" data-i18n="[placeholder]( None )">
</div> </div>
<div class="world_entry_form_control flex1" title="Defines delay levels for recursive scans.&#13;&#13;Initially, only the first level (smallest number) will match.&#13;Once no matches are found, the next level becomes eligible for matching.&#13;This repeats until all levels are checked.&#13;&#13;Tied to the &quot;Delay until recursion&quot; setting." data-i18n="[title]delay_until_recursion_level">
<small class="textAlignCenter">
<span data-i18n="Recursion Level">Recursion Level</span>
<div class="fa-solid fa-circle-info opacity50p"></div>
</small>
<input class="text_pole margin0" name="delayUntilRecursionLevel" type="text" placeholder="1">
</div>
</div> </div>
<div name="contentAndCharFilterBlock" class="world_entry_thin_controls flex2"> <div name="contentAndCharFilterBlock" class="world_entry_thin_controls flex2">
<div class="world_entry_form_control flex1"> <div class="world_entry_form_control flex1">
@ -5514,20 +5521,20 @@
<div> <div>
<label class="checkbox flex-container alignitemscenter flexNoGap"> <label class="checkbox flex-container alignitemscenter flexNoGap">
<input type="checkbox" name="exclude_recursion" /> <input type="checkbox" name="exclude_recursion" />
<span data-i18n="Exclude from recursion"> <span data-i18n="Non-recursable (will not be activated by another)">
Non-recursable (this entry will not be activated by another) Non-recursable (will not be activated by another)
</span> </span>
</label> </label>
<label class="checkbox flex-container alignitemscenter flexNoGap"> <label class="checkbox flex-container alignitemscenter flexNoGap">
<input type="checkbox" name="prevent_recursion" /> <input type="checkbox" name="prevent_recursion" />
<span data-i18n="Prevent further recursion (this entry will not activate others)"> <span data-i18n="Prevent further recursion (this entry will not activate others)">
Prevent further recursion (this entry will not activate others) Prevent further recursion (will not activate others)
</span> </span>
</label> </label>
<label class="checkbox flex-container alignitemscenter flexNoGap"> <label class="checkbox flex-container alignitemscenter flexNoGap">
<input type="checkbox" name="delay_until_recursion" /> <input type="checkbox" name="delay_until_recursion" />
<span data-i18n="Delay until recursion (this entry can only be activated on recursive checking)"> <span data-i18n="Delay until recursion (can only be activated on recursive checking)">
Delay until recursion (this entry can only be activated on recursive checking) Delay until recursion (can only be activated on recursive checking)
</span> </span>
</label> </label>
</div> </div>

View File

@ -217,8 +217,9 @@ class WorldInfoBuffer {
depth = MAX_SCAN_DEPTH; depth = MAX_SCAN_DEPTH;
} }
const JOINER = '\n\x01'; const MATCHER = '\x01';
let result = this.#depthBuffer.slice(this.#startDepth, depth).join(JOINER); const JOINER = '\n' + MATCHER;
let result = MATCHER + this.#depthBuffer.slice(this.#startDepth, depth).join(JOINER);
if (this.#injectBuffer.length > 0) { if (this.#injectBuffer.length > 0) {
result += JOINER + this.#injectBuffer.join(JOINER); result += JOINER + this.#injectBuffer.join(JOINER);
@ -2949,16 +2950,39 @@ async function getWorldEntry(name, data, entry) {
preventRecursionInput.prop('checked', entry.preventRecursion).trigger('input'); preventRecursionInput.prop('checked', entry.preventRecursion).trigger('input');
// delay until recursion // delay until recursion
// delay until recursion level
const delayUntilRecursionInput = template.find('input[name="delay_until_recursion"]'); const delayUntilRecursionInput = template.find('input[name="delay_until_recursion"]');
delayUntilRecursionInput.data('uid', entry.uid); delayUntilRecursionInput.data('uid', entry.uid);
const delayUntilRecursionLevelInput = template.find('input[name="delayUntilRecursionLevel"]');
delayUntilRecursionLevelInput.data('uid', entry.uid);
delayUntilRecursionInput.on('input', async function () { delayUntilRecursionInput.on('input', async function () {
const uid = $(this).data('uid'); const uid = $(this).data('uid');
const value = $(this).prop('checked'); const toggled = $(this).prop('checked');
// If the value contains a number, we'll take that one (set by the level input), otherwise we can use true/false switch
const value = toggled ? data.entries[uid].delayUntilRecursion || true : false;
if (!toggled) delayUntilRecursionLevelInput.val('');
data.entries[uid].delayUntilRecursion = value; data.entries[uid].delayUntilRecursion = value;
setWIOriginalDataValue(data, uid, 'extensions.delay_until_recursion', data.entries[uid].delayUntilRecursion); setWIOriginalDataValue(data, uid, 'extensions.delay_until_recursion', data.entries[uid].delayUntilRecursion);
await saveWorldInfo(name, data); await saveWorldInfo(name, data);
}); });
delayUntilRecursionInput.prop('checked', entry.delayUntilRecursion).trigger('input'); delayUntilRecursionInput.prop('checked', entry.delayUntilRecursion).trigger('input');
delayUntilRecursionLevelInput.on('input', async function () {
const uid = $(this).data('uid');
const content = $(this).val();
const value = content === '' ? (typeof data.entries[uid].delayUntilRecursion === 'boolean' ? data.entries[uid].delayUntilRecursion : true)
: content === 1 ? true
: !isNaN(Number(content)) ? Number(content)
: false;
data.entries[uid].delayUntilRecursion = value;
setWIOriginalDataValue(data, uid, 'extensions.delay_until_recursion', data.entries[uid].delayUntilRecursion);
await saveWorldInfo(name, data);
});
// No need to retrigger inpout event, we'll just set the curret current value. It was edited/saved above already
delayUntilRecursionLevelInput.val(['number', 'string'].includes(typeof entry.delayUntilRecursion) ? entry.delayUntilRecursion : '').trigger('input');
// duplicate button // duplicate button
const duplicateButton = template.find('.duplicate_entry_button'); const duplicateButton = template.find('.duplicate_entry_button');
@ -3257,7 +3281,7 @@ export const newWorldInfoEntryDefinition = {
disable: { default: false, type: 'boolean' }, disable: { default: false, type: 'boolean' },
excludeRecursion: { default: false, type: 'boolean' }, excludeRecursion: { default: false, type: 'boolean' },
preventRecursion: { default: false, type: 'boolean' }, preventRecursion: { default: false, type: 'boolean' },
delayUntilRecursion: { default: false, type: 'boolean' }, delayUntilRecursion: { default: 0, type: 'number' },
probability: { default: 100, type: 'number' }, probability: { default: 100, type: 'number' },
useProbability: { default: true, type: 'boolean' }, useProbability: { default: true, type: 'boolean' },
depth: { default: DEFAULT_DEPTH, type: 'number' }, depth: { default: DEFAULT_DEPTH, type: 'number' },
@ -3696,6 +3720,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
} }
} }
/** @type {scan_state} */
let scanState = scan_state.INITIAL; let scanState = scan_state.INITIAL;
let token_budget_overflowed = false; let token_budget_overflowed = false;
let count = 0; let count = 0;
@ -3720,6 +3745,17 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], allActivatedEntries: new Set() }; return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], allActivatedEntries: new Set() };
} }
/** @type {number[]} Represents the delay levels for entries that are delayed until recursion */
const availableRecursionDelayLevels = [...new Set(sortedEntries
.filter(entry => entry.delayUntilRecursion)
.map(entry => entry.delayUntilRecursion === true ? 1 : entry.delayUntilRecursion),
)].sort((a, b) => a - b);
// Already preset with the first level
let currentRecursionDelayLevel = availableRecursionDelayLevels.shift() ?? 0;
if (currentRecursionDelayLevel > 0 && availableRecursionDelayLevels.length) {
console.debug('[WI] Preparing first delayed recursion level', currentRecursionDelayLevel, '. Still delayed:', availableRecursionDelayLevels);
}
console.debug(`[WI] --- SEARCHING ENTRIES (on ${sortedEntries.length} entries) ---`); console.debug(`[WI] --- SEARCHING ENTRIES (on ${sortedEntries.length} entries) ---`);
while (scanState) { while (scanState) {
@ -3732,7 +3768,8 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
// Track how many times the loop has run. May be useful for debugging. // Track how many times the loop has run. May be useful for debugging.
count++; count++;
console.debug(`[WI] Loop #${count}. Search state`, Object.entries(scan_state).find(x => x[1] === scanState)); console.debug(`[WI] --- LOOP #${count} START ---`);
console.debug('[WI] Scan state', Object.entries(scan_state).find(x => x[1] === scanState));
// Until decided otherwise, we set the loop to stop scanning after this // Until decided otherwise, we set the loop to stop scanning after this
let nextScanState = scan_state.NONE; let nextScanState = scan_state.NONE;
@ -3811,6 +3848,11 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
continue; continue;
} }
if (scanState === scan_state.RECURSION && entry.delayUntilRecursion && entry.delayUntilRecursion > currentRecursionDelayLevel && !isSticky) {
log('suppressed by delay until recursion level', entry.delayUntilRecursion, '. Currently', currentRecursionDelayLevel);
continue;
}
if (scanState === scan_state.RECURSION && world_info_recursive && entry.excludeRecursion && !isSticky) { if (scanState === scan_state.RECURSION && world_info_recursive && entry.excludeRecursion && !isSticky) {
log('suppressed by exclude recursion'); log('suppressed by exclude recursion');
continue; continue;
@ -3949,6 +3991,8 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanState, timedEffects); filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanState, timedEffects);
console.debug('[WI] --- PROBABILITY CHECKS ---'); console.debug('[WI] --- PROBABILITY CHECKS ---');
!newEntries.length && console.debug('[WI] No probability checks to do');
for (const entry of newEntries) { for (const entry of newEntries) {
function verifyProbability() { function verifyProbability() {
// If we don't need to roll, it's always true // If we don't need to roll, it's always true
@ -3984,6 +4028,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
newContent += `${entry.content}\n`; newContent += `${entry.content}\n`;
if ((textToScanTokens + (await getTokenCountAsync(newContent))) >= budget) { if ((textToScanTokens + (await getTokenCountAsync(newContent))) >= budget) {
console.debug('[WI] --- BUDGET OVERFLOW CHECK ---');
if (world_info_overflow_alert) { if (world_info_overflow_alert) {
console.warn(`[WI] budget of ${budget} reached, stopping after ${allActivatedEntries.size} entries`); console.warn(`[WI] budget of ${budget} reached, stopping after ${allActivatedEntries.size} entries`);
toastr.warning(`World info budget reached after ${allActivatedEntries.size} entries.`, 'World Info'); toastr.warning(`World info budget reached after ${allActivatedEntries.size} entries.`, 'World Info');
@ -4001,23 +4046,31 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
const successfulNewEntries = newEntries.filter(x => !failedProbabilityChecks.has(x)); const successfulNewEntries = newEntries.filter(x => !failedProbabilityChecks.has(x));
const successfulNewEntriesForRecursion = successfulNewEntries.filter(x => !x.preventRecursion); const successfulNewEntriesForRecursion = successfulNewEntries.filter(x => !x.preventRecursion);
console.debug(`[WI] --- LOOP #${count} RESULT ---`);
if (!newEntries.length) { if (!newEntries.length) {
console.debug('[WI] No new entries activated, stopping'); console.debug('[WI] No new entries activated.');
} else if (!successfulNewEntries.length) { } else if (!successfulNewEntries.length) {
console.debug('[WI] Probability checks failed for all activated entries, stopping'); console.debug('[WI] Probability checks failed for all activated entries. No new entries activated.');
} else { } else {
console.debug(`[WI] Successfully activated ${successfulNewEntries.length} new entries to prompt. ${allActivatedEntries.size} total entries activated.`, successfulNewEntries); console.debug(`[WI] Successfully activated ${successfulNewEntries.length} new entries to prompt. ${allActivatedEntries.size} total entries activated.`, successfulNewEntries);
} }
function logNextState(...args) {
args.length && console.debug(args.shift(), ...args);
console.debug('[WI] Setting scan state', Object.entries(scan_state).find(x => x[1] === scanState));
}
// After processing and rolling entries is done, see if we should continue with normal recursion // After processing and rolling entries is done, see if we should continue with normal recursion
if (world_info_recursive && !token_budget_overflowed && successfulNewEntriesForRecursion.length) { if (world_info_recursive && !token_budget_overflowed && successfulNewEntriesForRecursion.length) {
nextScanState = scan_state.RECURSION; nextScanState = scan_state.RECURSION;
logNextState('[WI] Found', successfulNewEntriesForRecursion.length, 'new entries for recursion');
} }
// If we are inside min activations scan, and we have recursive buffer, we should do a recursive scan before increasing the buffer again // If we are inside min activations scan, and we have recursive buffer, we should do a recursive scan before increasing the buffer again
// There might be recurse-trigger-able entries that match the buffer, so we need to check that // There might be recurse-trigger-able entries that match the buffer, so we need to check that
if (world_info_recursive && !token_budget_overflowed && scanState === scan_state.MIN_ACTIVATIONS && buffer.hasRecurse()) { if (world_info_recursive && !token_budget_overflowed && scanState === scan_state.MIN_ACTIVATIONS && buffer.hasRecurse()) {
nextScanState = scan_state.RECURSION; nextScanState = scan_state.RECURSION;
logNextState('[WI] Min Activations run done, whill will always be followed by a recursive scan');
} }
// If scanning is planned to stop, but min activations is set and not satisfied, check if we should continue // If scanning is planned to stop, but min activations is set and not satisfied, check if we should continue
@ -4031,14 +4084,21 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
) || (buffer.getDepth() > chat.length); ) || (buffer.getDepth() > chat.length);
if (!over_max) { if (!over_max) {
console.debug(`[WI] Min activations not reached (${allActivatedEntries.size}/${world_info_min_activations}), advancing depth to ${buffer.getDepth() + 1} and checking again`);
nextScanState = scan_state.MIN_ACTIVATIONS; // loop nextScanState = scan_state.MIN_ACTIVATIONS; // loop
logNextState(`[WI] Min activations not reached (${allActivatedEntries.size}/${world_info_min_activations}), advancing depth to ${buffer.getDepth() + 1}, starting another scan`);
buffer.advanceScan(); buffer.advanceScan();
} else { } else {
console.debug(`[WI] Min activations not reached (${allActivatedEntries.size}/${world_info_min_activations}), but reached on of depth. Stopping`); console.debug(`[WI] Min activations not reached (${allActivatedEntries.size}/${world_info_min_activations}), but reached on of depth. Stopping`);
} }
} }
// If the scan is done, but we still have open "delay until recursion" levels, we should continue with the next one
if (nextScanState === scan_state.NONE && availableRecursionDelayLevels.length) {
nextScanState = scan_state.RECURSION;
currentRecursionDelayLevel = availableRecursionDelayLevels.shift();
logNextState('[WI] Open delayed recursion levels left. Preparing next delayed recursion level', currentRecursionDelayLevel, '. Still delayed:', availableRecursionDelayLevels);
}
// Final check if we should really continue scan, and extend the current WI recurse buffer // Final check if we should really continue scan, and extend the current WI recurse buffer
scanState = nextScanState; scanState = nextScanState;
if (scanState) { if (scanState) {
@ -4046,6 +4106,8 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
.map(x => x.content).join('\n'); .map(x => x.content).join('\n');
buffer.addRecurse(text); buffer.addRecurse(text);
allActivatedText = (text + '\n' + allActivatedText); allActivatedText = (text + '\n' + allActivatedText);
} else {
logNextState('[WI] Scan done. No new entries to prompt. Stopping.');
} }
} }