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;
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">
<small class="textAlignCenter" data-i18n="Case-Sensitive">Case-Sensitive</small>
<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="false" data-i18n="No">No</option>
</select>
</div>
<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">
<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="false" data-i18n="No">No</option>
</select>
</div>
<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">
<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="false" data-i18n="No">No</option>
</select>
@ -5498,6 +5498,13 @@
<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 )">
</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 name="contentAndCharFilterBlock" class="world_entry_thin_controls flex2">
<div class="world_entry_form_control flex1">
@ -5514,20 +5521,20 @@
<div>
<label class="checkbox flex-container alignitemscenter flexNoGap">
<input type="checkbox" name="exclude_recursion" />
<span data-i18n="Exclude from recursion">
Non-recursable (this entry will not be activated by another)
<span data-i18n="Non-recursable (will not be activated by another)">
Non-recursable (will not be activated by another)
</span>
</label>
<label class="checkbox flex-container alignitemscenter flexNoGap">
<input type="checkbox" name="prevent_recursion" />
<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>
</label>
<label class="checkbox flex-container alignitemscenter flexNoGap">
<input type="checkbox" name="delay_until_recursion" />
<span data-i18n="Delay until recursion (this entry can only be activated on recursive checking)">
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 (can only be activated on recursive checking)
</span>
</label>
</div>

View File

@ -217,8 +217,9 @@ class WorldInfoBuffer {
depth = MAX_SCAN_DEPTH;
}
const JOINER = '\n\x01';
let result = this.#depthBuffer.slice(this.#startDepth, depth).join(JOINER);
const MATCHER = '\x01';
const JOINER = '\n' + MATCHER;
let result = MATCHER + this.#depthBuffer.slice(this.#startDepth, depth).join(JOINER);
if (this.#injectBuffer.length > 0) {
result += JOINER + this.#injectBuffer.join(JOINER);
@ -2949,16 +2950,39 @@ async function getWorldEntry(name, data, entry) {
preventRecursionInput.prop('checked', entry.preventRecursion).trigger('input');
// delay until recursion
// delay until recursion level
const delayUntilRecursionInput = template.find('input[name="delay_until_recursion"]');
delayUntilRecursionInput.data('uid', entry.uid);
const delayUntilRecursionLevelInput = template.find('input[name="delayUntilRecursionLevel"]');
delayUntilRecursionLevelInput.data('uid', entry.uid);
delayUntilRecursionInput.on('input', async function () {
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;
setWIOriginalDataValue(data, uid, 'extensions.delay_until_recursion', data.entries[uid].delayUntilRecursion);
await saveWorldInfo(name, data);
});
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
const duplicateButton = template.find('.duplicate_entry_button');
@ -3257,7 +3281,7 @@ export const newWorldInfoEntryDefinition = {
disable: { default: false, type: 'boolean' },
excludeRecursion: { default: false, type: 'boolean' },
preventRecursion: { default: false, type: 'boolean' },
delayUntilRecursion: { default: false, type: 'boolean' },
delayUntilRecursion: { default: 0, type: 'number' },
probability: { default: 100, type: 'number' },
useProbability: { default: true, type: 'boolean' },
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 token_budget_overflowed = false;
let count = 0;
@ -3720,6 +3745,17 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
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) ---`);
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.
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
let nextScanState = scan_state.NONE;
@ -3811,6 +3848,11 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
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) {
log('suppressed by exclude recursion');
continue;
@ -3949,6 +3991,8 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanState, timedEffects);
console.debug('[WI] --- PROBABILITY CHECKS ---');
!newEntries.length && console.debug('[WI] No probability checks to do');
for (const entry of newEntries) {
function verifyProbability() {
// 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`;
if ((textToScanTokens + (await getTokenCountAsync(newContent))) >= budget) {
console.debug('[WI] --- BUDGET OVERFLOW CHECK ---');
if (world_info_overflow_alert) {
console.warn(`[WI] budget of ${budget} reached, stopping after ${allActivatedEntries.size} entries`);
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 successfulNewEntriesForRecursion = successfulNewEntries.filter(x => !x.preventRecursion);
console.debug(`[WI] --- LOOP #${count} RESULT ---`);
if (!newEntries.length) {
console.debug('[WI] No new entries activated, stopping');
console.debug('[WI] No new entries activated.');
} 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 {
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
if (world_info_recursive && !token_budget_overflowed && successfulNewEntriesForRecursion.length) {
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
// 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()) {
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
@ -4031,14 +4084,21 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
) || (buffer.getDepth() > chat.length);
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
logNextState(`[WI] Min activations not reached (${allActivatedEntries.size}/${world_info_min_activations}), advancing depth to ${buffer.getDepth() + 1}, starting another scan`);
buffer.advanceScan();
} else {
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
scanState = nextScanState;
if (scanState) {
@ -4046,6 +4106,8 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
.map(x => x.content).join('\n');
buffer.addRecurse(text);
allActivatedText = (text + '\n' + allActivatedText);
} else {
logNextState('[WI] Scan done. No new entries to prompt. Stopping.');
}
}