Merge pull request #2479 from SillyTavern/wi-processing-refactoring WI scan refactoring: Extended logging, updated code flow, changed Min Activations

9268fc3ef2909f4359938d316e2617576be96083

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
1 files changed, +264 -161Ignore whitespace
public/scripts/world-info.js+264 -161
@@ -169,7 +169,7 @@ class WorldInfoBuffer {
169 #skew = 0;169 #skew = 0;
170170
171 /**171 /**
172 * @type {number} The starting depth of the global scan depth. Incremented by "min activations" feature to not repeat scans. When > 0 it means a complete scan was done up to #startDepth already, and `advanceScanPosition` was called.172 * @type {number} The starting depth of the global scan depth.
173 */173 */
174 #startDepth = 0;174 #startDepth = 0;
175175
@@ -222,12 +222,12 @@ class WorldInfoBuffer {
222 }222 }
223223
224 if (depth < 0) {224 if (depth < 0) {
225 console.error(`Invalid WI scan depth ${depth}. Must be >= 0`);225 console.error(`[WI] Invalid WI scan depth ${depth}. Must be >= 0`);
226 return '';226 return '';
227 }227 }
228228
229 if (depth > MAX_SCAN_DEPTH) {229 if (depth > MAX_SCAN_DEPTH) {
230 console.warn(`Invalid WI scan depth ${depth}. Truncating to ${MAX_SCAN_DEPTH}`);230 console.warn(`[WI] Invalid WI scan depth ${depth}. Truncating to ${MAX_SCAN_DEPTH}`);
231 depth = MAX_SCAN_DEPTH;231 depth = MAX_SCAN_DEPTH;
232 }232 }
233233
@@ -301,10 +301,17 @@ class WorldInfoBuffer {
301 }301 }
302302
303 /**303 /**
304 * Increments skew and sets startDepth to previous depth.304 * Checks if the recursion buffer is not empty.
305 * @returns {boolean} Returns true if the recursion buffer is not empty, otherwise false
305 */306 */
306 advanceScanPosition() {307 hasRecurse() {
307 this.#startDepth = this.getDepth();308 return this.#recurseBuffer.length > 0;
309 }
310
311 /**
312 * Increments skew to advance the scan range.
313 */
314 advanceScan() {
308 this.#skew++;315 this.#skew++;
309 }316 }
310317
@@ -436,7 +443,7 @@ class WorldInfoTimedEffects {
436 const key = this.#getEntryKey(entry);443 const key = this.#getEntryKey(entry);
437 const effect = this.#getEntryTimedEffect('cooldown', entry, true);444 const effect = this.#getEntryTimedEffect('cooldown', entry, true);
438 chat_metadata.timedWorldInfo.cooldown[key] = effect;445 chat_metadata.timedWorldInfo.cooldown[key] = effect;
439 console.log(`Adding cooldown entry ${key} on ended sticky: start=${effect.start}, end=${effect.end}, protected=${effect.protected}`);446 console.log(`[WI] Adding cooldown entry ${key} on ended sticky: start=${effect.start}, end=${effect.end}, protected=${effect.protected}`);
440 // Set the cooldown immediately for this evaluation447 // Set the cooldown immediately for this evaluation
441 this.#buffer.cooldown.push(entry);448 this.#buffer.cooldown.push(entry);
442 },449 },
@@ -447,10 +454,10 @@ class WorldInfoTimedEffects {
447 * @param {WIScanEntry} entry Entry that ended cooldown454 * @param {WIScanEntry} entry Entry that ended cooldown
448 */455 */
449 'cooldown': (entry) => {456 'cooldown': (entry) => {
450 console.debug('Cooldown ended for entry', entry.uid);457 console.debug('[WI] Cooldown ended for entry', entry.uid);
451 },458 },
452459
453 'delay': () => {},460 'delay': () => { },
454 };461 };
455462
456 /**463 /**
@@ -537,11 +544,11 @@ class WorldInfoTimedEffects {
537 /** @type {[string, WITimedEffect][]} */544 /** @type {[string, WITimedEffect][]} */
538 const effects = Object.entries(chat_metadata.timedWorldInfo[type]);545 const effects = Object.entries(chat_metadata.timedWorldInfo[type]);
539 for (const [key, value] of effects) {546 for (const [key, value] of effects) {
540 console.log(`Processing ${type} entry ${key}`, value);547 console.log(`[WI] Processing ${type} entry ${key}`, value);
541 const entry = this.#entries.find(x => String(this.#getEntryHash(x)) === String(value.hash));548 const entry = this.#entries.find(x => String(this.#getEntryHash(x)) === String(value.hash));
542549
543 if (this.#chat.length <= Number(value.start) && !value.protected) {550 if (this.#chat.length <= Number(value.start) && !value.protected) {
544 console.log(`Removing ${type} entry ${key} from timedWorldInfo: chat not advanced`, value);551 console.log(`[WI] Removing ${type} entry ${key} from timedWorldInfo: chat not advanced`, value);
545 delete chat_metadata.timedWorldInfo[type][key];552 delete chat_metadata.timedWorldInfo[type][key];
546 continue;553 continue;
547 }554 }
@@ -549,7 +556,7 @@ class WorldInfoTimedEffects {
549 // Missing entries (they could be from another character's lorebook)556 // Missing entries (they could be from another character's lorebook)
550 if (!entry) {557 if (!entry) {
551 if (this.#chat.length >= Number(value.end)) {558 if (this.#chat.length >= Number(value.end)) {
552 console.log(`Removing ${type} entry from timedWorldInfo: entry not found and interval passed`, entry);559 console.log(`[WI] Removing ${type} entry from timedWorldInfo: entry not found and interval passed`, entry);
553 delete chat_metadata.timedWorldInfo[type][key];560 delete chat_metadata.timedWorldInfo[type][key];
554 }561 }
555 continue;562 continue;
@@ -557,13 +564,13 @@ class WorldInfoTimedEffects {
557564
558 // Ignore invalid entries (not configured for timed effects)565 // Ignore invalid entries (not configured for timed effects)
559 if (!entry[type]) {566 if (!entry[type]) {
560 console.log(`Removing ${type} entry from timedWorldInfo: entry not ${type}`, entry);567 console.log(`[WI] Removing ${type} entry from timedWorldInfo: entry not ${type}`, entry);
561 delete chat_metadata.timedWorldInfo[type][key];568 delete chat_metadata.timedWorldInfo[type][key];
562 continue;569 continue;
563 }570 }
564571
565 if (this.#chat.length >= Number(value.end)) {572 if (this.#chat.length >= Number(value.end)) {
566 console.log(`Removing ${type} entry from timedWorldInfo: ${type} interval passed`, entry);573 console.log(`[WI] Removing ${type} entry from timedWorldInfo: ${type} interval passed`, entry);
567 delete chat_metadata.timedWorldInfo[type][key];574 delete chat_metadata.timedWorldInfo[type][key];
568 if (typeof onEnded === 'function') {575 if (typeof onEnded === 'function') {
569 onEnded(entry);576 onEnded(entry);
@@ -572,7 +579,7 @@ class WorldInfoTimedEffects {
572 }579 }
573580
574 buffer.push(entry);581 buffer.push(entry);
575 console.log(`Timed effect "${type}" applied to entry`, entry);582 console.log(`[WI] Timed effect "${type}" applied to entry`, entry);
576 }583 }
577 }584 }
578585
@@ -588,7 +595,7 @@ class WorldInfoTimedEffects {
588595
589 if (this.#chat.length < entry.delay) {596 if (this.#chat.length < entry.delay) {
590 buffer.push(entry);597 buffer.push(entry);
591 console.log('Timed effect "delay" applied to entry', entry);598 console.log('[WI] Timed effect "delay" applied to entry', entry);
592 }599 }
593 }600 }
594601
@@ -635,7 +642,7 @@ class WorldInfoTimedEffects {
635 const effect = this.#getEntryTimedEffect(type, entry, false);642 const effect = this.#getEntryTimedEffect(type, entry, false);
636 chat_metadata.timedWorldInfo[type][key] = effect;643 chat_metadata.timedWorldInfo[type][key] = effect;
637644
638 console.log(`Adding ${type} entry ${key}: start=${effect.start}, end=${effect.end}, protected=${effect.protected}`);645 console.log(`[WI] Adding ${type} entry ${key}: start=${effect.start}, end=${effect.end}, protected=${effect.protected}`);
639 }646 }
640 }647 }
641648
@@ -667,7 +674,7 @@ class WorldInfoTimedEffects {
667 if (newState) {674 if (newState) {
668 const effect = this.#getEntryTimedEffect(type, entry, false);675 const effect = this.#getEntryTimedEffect(type, entry, false);
669 chat_metadata.timedWorldInfo[type][key] = effect;676 chat_metadata.timedWorldInfo[type][key] = effect;
670 console.log(`Adding ${type} entry ${key}: start=${effect.start}, end=${effect.end}, protected=${effect.protected}`);677 console.log(`[WI] Adding ${type} entry ${key}: start=${effect.start}, end=${effect.end}, protected=${effect.protected}`);
671 }678 }
672 }679 }
673680
@@ -3419,13 +3426,12 @@ async function createNewWorldInfo(worldName, { interactive = false } = {}) {
3419async function getCharacterLore() {3426async function getCharacterLore() {
3420 const character = characters[this_chid];3427 const character = characters[this_chid];
3421 const name = character?.name;3428 const name = character?.name;
3429 /** @type {Set<string>} */
3422 let worldsToSearch = new Set();3430 let worldsToSearch = new Set();
34233431
3424 const baseWorldName = character?.data?.extensions?.world;3432 const baseWorldName = character?.data?.extensions?.world;
3425 if (baseWorldName) {3433 if (baseWorldName) {
3426 worldsToSearch.add(baseWorldName);3434 worldsToSearch.add(baseWorldName);
3427 } else {
3428 console.debug(`Character ${name}'s base world could not be found or is empty! Skipping...`);
3429 }3435 }
34303436
3431 // TODO: Maybe make the utility function not use the window context?3437 // TODO: Maybe make the utility function not use the window context?
@@ -3435,40 +3441,48 @@ async function getCharacterLore() {
3435 worldsToSearch = new Set([...worldsToSearch, ...extraCharLore.extraBooks]);3441 worldsToSearch = new Set([...worldsToSearch, ...extraCharLore.extraBooks]);
3436 }3442 }
34373443
3444 if (!worldsToSearch.size) {
3445 return [];
3446 }
3447
3438 let entries = [];3448 let entries = [];
3439 for (const worldName of worldsToSearch) {3449 for (const worldName of worldsToSearch) {
3440 if (selected_world_info.includes(worldName)) {3450 if (selected_world_info.includes(worldName)) {
3441 console.debug(`Character ${name}'s world ${worldName} is already activated in global world info! Skipping...`);3451 console.debug(`[WI] Character ${name}'s world ${worldName} is already activated in global world info! Skipping...`);
3442 continue;3452 continue;
3443 }3453 }
34443454
3445 if (chat_metadata[METADATA_KEY] === worldName) {3455 if (chat_metadata[METADATA_KEY] === worldName) {
3446 console.debug(`Character ${name}'s world ${worldName} is already activated in chat lore! Skipping...`);3456 console.debug(`[WI] Character ${name}'s world ${worldName} is already activated in chat lore! Skipping...`);
3447 continue;3457 continue;
3448 }3458 }
34493459
3450 const data = await loadWorldInfoData(worldName);3460 const data = await loadWorldInfoData(worldName);
3451 const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(x => ({ ...x, world: worldName })) : [];3461 const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: worldName, ...rest })) : [];
3452 entries = entries.concat(newEntries);3462 entries = entries.concat(newEntries);
3463
3464 if (!newEntries.length) {
3465 console.debug(`[WI] Character ${name}'s world ${worldName} could not be found or is empty`);
3466 }
3453 }3467 }
34543468
3455 console.debug(`Character ${name} lore (${Array.from(worldsToSearch)}) has ${entries.length} world info entries`);3469 console.debug(`[WI] Character ${name}'s lore has ${entries.length} world info entries`, [...worldsToSearch]);
3456 return entries;3470 return entries;
3457}3471}
34583472
3459async function getGlobalLore() {3473async function getGlobalLore() {
3460 if (!selected_world_info) {3474 if (!selected_world_info?.length) {
3461 return [];3475 return [];
3462 }3476 }
34633477
3464 let entries = [];3478 let entries = [];
3465 for (const worldName of selected_world_info) {3479 for (const worldName of selected_world_info) {
3466 const data = await loadWorldInfoData(worldName);3480 const data = await loadWorldInfoData(worldName);
3467 const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(x => ({ ...x, world: worldName })) : [];3481 const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: worldName, ...rest })) : [];
3468 entries = entries.concat(newEntries);3482 entries = entries.concat(newEntries);
3469 }3483 }
34703484
3471 console.debug(`Global world info has ${entries.length} entries`);3485 console.debug(`[WI] Global world info has ${entries.length} entries`, selected_world_info);
34723486
3473 return entries;3487 return entries;
3474}3488}
@@ -3481,14 +3495,14 @@ async function getChatLore() {
3481 }3495 }
34823496
3483 if (selected_world_info.includes(chatWorld)) {3497 if (selected_world_info.includes(chatWorld)) {
3484 console.debug(`Chat world ${chatWorld} is already activated in global world info! Skipping...`);3498 console.debug(`[WI] Chat world ${chatWorld} is already activated in global world info! Skipping...`);
3485 return [];3499 return [];
3486 }3500 }
34873501
3488 const data = await loadWorldInfoData(chatWorld);3502 const data = await loadWorldInfoData(chatWorld);
3489 const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(x => ({ ...x, world: chatWorld })) : [];3503 const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: chatWorld, ...rest })) : [];
34903504
3491 console.debug(`Chat lore has ${entries.length} entries`);3505 console.debug(`[WI] Chat lore has ${entries.length} entries`, [chatWorld]);
34923506
3493 return entries;3507 return entries;
3494}3508}
@@ -3512,7 +3526,7 @@ export async function getSortedEntries() {
3512 entries = [...globalLore.sort(sortFn), ...characterLore.sort(sortFn)];3526 entries = [...globalLore.sort(sortFn), ...characterLore.sort(sortFn)];
3513 break;3527 break;
3514 default:3528 default:
3515 console.error('Unknown WI insertion strategy: ', world_info_character_strategy, 'defaulting to evenly');3529 console.error('[WI] Unknown WI insertion strategy:', world_info_character_strategy, 'defaulting to evenly');
3516 entries = [...globalLore, ...characterLore].sort(sortFn);3530 entries = [...globalLore, ...characterLore].sort(sortFn);
3517 break;3531 break;
3518 }3532 }
@@ -3520,7 +3534,7 @@ export async function getSortedEntries() {
3520 // Chat lore always goes first3534 // Chat lore always goes first
3521 entries = [...chatLore.sort(sortFn), ...entries];3535 entries = [...chatLore.sort(sortFn), ...entries];
35223536
3523 console.debug(`Sorted ${entries.length} world lore entries using strategy ${world_info_character_strategy}`);3537 console.debug(`[WI] Found ${entries.length} world lore entries. Sorted by strategy`, Object.entries(world_info_insertion_strategy).find((x) => x[1] === world_info_character_strategy));
35243538
3525 // Need to deep clone the entries to avoid modifying the cached data3539 // Need to deep clone the entries to avoid modifying the cached data
3526 return structuredClone(entries);3540 return structuredClone(entries);
@@ -3543,6 +3557,8 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
3543 const context = getContext();3557 const context = getContext();
3544 const buffer = new WorldInfoBuffer(chat);3558 const buffer = new WorldInfoBuffer(chat);
35453559
3560 console.debug(`[WI] --- START WI SCAN (on ${chat.length} messages) ---`);
3561
3546 // Combine the chat3562 // Combine the chat
35473563
3548 // Add the depth or AN if enabled3564 // Add the depth or AN if enabled
@@ -3566,11 +3582,11 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
3566 let budget = Math.round(world_info_budget * maxContext / 100) || 1;3582 let budget = Math.round(world_info_budget * maxContext / 100) || 1;
35673583
3568 if (world_info_budget_cap > 0 && budget > world_info_budget_cap) {3584 if (world_info_budget_cap > 0 && budget > world_info_budget_cap) {
3569 console.debug(`Budget ${budget} exceeds cap ${world_info_budget_cap}, using cap`);3585 console.debug(`[WI] Budget ${budget} exceeds cap ${world_info_budget_cap}, using cap`);
3570 budget = world_info_budget_cap;3586 budget = world_info_budget_cap;
3571 }3587 }
35723588
3573 console.debug(`Context size: ${maxContext}; WI budget: ${budget} (max% = ${world_info_budget}%, cap = ${world_info_budget_cap})`);3589 console.debug(`[WI] Context size: ${maxContext}; WI budget: ${budget} (max% = ${world_info_budget}%, cap = ${world_info_budget_cap})`);
3574 const sortedEntries = await getSortedEntries();3590 const sortedEntries = await getSortedEntries();
3575 const timedEffects = new WorldInfoTimedEffects(chat, sortedEntries);3591 const timedEffects = new WorldInfoTimedEffects(chat, sortedEntries);
35763592
@@ -3580,21 +3596,48 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
3580 return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], allActivatedEntries: new Set() };3596 return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], allActivatedEntries: new Set() };
3581 }3597 }
35823598
3599 console.debug(`[WI] --- SEARCHING ENTRIES (on ${sortedEntries.length} entries) ---`);
3600
3583 while (scanState) {3601 while (scanState) {
3584 // Track how many times the loop has run. May be useful for debugging.3602 // Track how many times the loop has run. May be useful for debugging.
3585 // eslint-disable-next-line no-unused-vars
3586 count++;3603 count++;
35873604
3605 console.debug(`[WI] Loop #${count}. Search state`, Object.entries(scan_state).find(x => x[1] === scanState));
3606
3607 // Until decided otherwise, we set the loop to stop scanning after this
3608 let nextScanState = scan_state.NONE;
3609
3610 // Loop and find all entries that can activate here
3588 let activatedNow = new Set();3611 let activatedNow = new Set();
35893612
3590 for (let entry of sortedEntries) {3613 for (let entry of sortedEntries) {
3614 // Logging preparation
3615 let headerLogged = false;
3616 function log(...args) {
3617 if (!headerLogged) {
3618 console.debug(`[WI] Entry ${entry.uid}`, `from '${entry.world}' processing`, entry);
3619 headerLogged = true;
3620 }
3621 console.debug(`[WI] Entry ${entry.uid}`, ...args);
3622 }
3623
3624 // Already processed, considered and then skipped entries should still be skipped
3625 if (failedProbabilityChecks.has(entry) || allActivatedEntries.has(entry)) {
3626 continue;
3627 }
3628
3629 if (entry.disable == true) {
3630 log('disabled');
3631 continue;
3632 }
3633
3591 // Check if this entry applies to the character or if it's excluded3634 // Check if this entry applies to the character or if it's excluded
3592 if (entry.characterFilter && entry.characterFilter?.names?.length > 0) {3635 if (entry.characterFilter && entry.characterFilter?.names?.length > 0) {
3593 const nameIncluded = entry.characterFilter.names.includes(getCharaFilename());3636 const nameIncluded = entry.characterFilter.names.includes(getCharaFilename());
3594 const filtered = entry.characterFilter.isExclude ? nameIncluded : !nameIncluded;3637 const filtered = entry.characterFilter.isExclude ? nameIncluded : !nameIncluded;
35953638
3596 if (filtered) {3639 if (filtered) {
3597 console.debug(`WI entry ${entry.uid} filtered out by character`);3640 log('filtered out by character');
3598 continue;3641 continue;
3599 }3642 }
3600 }3643 }
@@ -3611,7 +3654,7 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
3611 const filtered = entry.characterFilter.isExclude ? includesTag : !includesTag;3654 const filtered = entry.characterFilter.isExclude ? includesTag : !includesTag;
36123655
3613 if (filtered) {3656 if (filtered) {
3614 console.debug(`WI entry ${entry.uid} filtered out by tag`);3657 log('filtered out by tag');
3615 continue;3658 continue;
3616 }3659 }
3617 }3660 }
@@ -3623,186 +3666,242 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
3623 const isDelay = timedEffects.isEffectActive('delay', entry);3666 const isDelay = timedEffects.isEffectActive('delay', entry);
36243667
3625 if (isDelay) {3668 if (isDelay) {
3626 console.debug(`WI entry ${entry.uid} suppressed by delay`, entry);3669 log('suppressed by delay');
3627 continue;3670 continue;
3628 }3671 }
36293672
3630 if (isCooldown && !isSticky) {3673 if (isCooldown && !isSticky) {
3631 console.debug(`WI entry ${entry.uid} suppressed by cooldown`, entry);3674 log('suppressed by cooldown');
3632 continue;3675 continue;
3633 }3676 }
36343677
3635 if (failedProbabilityChecks.has(entry)) {3678 // Only use checks for recursion flags if the scan step was activated by recursion
3679 if (scanState !== scan_state.RECURSION && entry.delayUntilRecursion) {
3680 log('suppressed by delay until recursion');
3636 continue;3681 continue;
3637 }3682 }
36383683
3639 if (allActivatedEntries.has(entry) || entry.disable == true) {3684 if (scanState === scan_state.RECURSION && world_info_recursive && entry.excludeRecursion) {
3685 log('suppressed by exclude recursion');
3640 continue;3686 continue;
3641 }3687 }
36423688
3643 // Only use checks for recursion flags if the scan step was activated by recursion3689 // Now do checks for immediate activations
3644 if (scanState !== scan_state.RECURSION && entry.delayUntilRecursion) {3690 if (entry.constant) {
3645 console.debug(`WI entry ${entry.uid} suppressed by delay until recursion`, entry);3691 log('activated because of constant');
3692 activatedNow.add(entry);
3646 continue;3693 continue;
3647 }3694 }
36483695
3649 if (scanState === scan_state.RECURSION && world_info_recursive && entry.excludeRecursion) {3696 if (buffer.isExternallyActivated(entry)) {
3650 console.debug(`WI entry ${entry.uid} suppressed by exclude recursion`, entry);3697 log('externally activated');
3698 activatedNow.add(entry);
3651 continue;3699 continue;
3652 }3700 }
36533701
3654 if (entry.constant || buffer.isExternallyActivated(entry) || isSticky) {3702 if (isSticky) {
3703 log('activated because active sticky');
3655 activatedNow.add(entry);3704 activatedNow.add(entry);
3656 continue;3705 continue;
3657 }3706 }
36583707
3659 if (Array.isArray(entry.key) && entry.key.length) { //check for keywords existing3708 if (!Array.isArray(entry.key) || !entry.key.length) {
3660 // If selectiveLogic isn't found, assume it's AND, only do this once per entry3709 log('has no keys defined, skipped');
3661 const selectiveLogic = entry.selectiveLogic ?? 0;3710 continue;
36623711 }
3663 primary: for (let key of entry.key) {
3664 const substituted = substituteParams(key);
3665 const textToScan = buffer.get(entry, scanState);
3666
3667 if (substituted && buffer.matchKeys(textToScan, substituted.trim(), entry)) {
3668 console.debug(`WI UID ${entry.uid} found by primary match: ${substituted}.`);
3669
3670 //selective logic begins
3671 if (
3672 entry.selective && //all entries are selective now
3673 Array.isArray(entry.keysecondary) && //always true
3674 entry.keysecondary.length //ignore empties
3675 ) {
3676 console.debug(`WI UID:${entry.uid} found. Checking logic: ${entry.selectiveLogic}`);
3677 let hasAnyMatch = false;
3678 let hasAllMatch = true;
3679 secondary: for (let keysecondary of entry.keysecondary) {
3680 const secondarySubstituted = substituteParams(keysecondary);
3681 const hasSecondaryMatch = secondarySubstituted && buffer.matchKeys(textToScan, secondarySubstituted.trim(), entry);
3682 console.debug(`WI UID:${entry.uid}: Filtering for secondary keyword - "${secondarySubstituted}".`);
3683
3684 if (hasSecondaryMatch) {
3685 hasAnyMatch = true;
3686 }
3687
3688 if (!hasSecondaryMatch) {
3689 hasAllMatch = false;
3690 }
3691
3692 // Simplified AND ANY / NOT ALL if statement. (Proper fix for PR#1356 by Bronya)
3693 // If AND ANY logic and the main checks pass OR if NOT ALL logic and the main checks do not pass
3694 if ((selectiveLogic === world_info_logic.AND_ANY && hasSecondaryMatch) || (selectiveLogic === world_info_logic.NOT_ALL && !hasSecondaryMatch)) {
3695 // Differ both logic statements in the debugger
3696 if (selectiveLogic === world_info_logic.AND_ANY) {
3697 console.debug(`(AND ANY Check) Activating WI Entry ${entry.uid}. Found match for word: ${substituted} ${secondarySubstituted}`);
3698 } else {
3699 console.debug(`(NOT ALL Check) Activating WI Entry ${entry.uid}. Found match for word "${substituted}" without secondary keyword: ${secondarySubstituted}`);
3700 }
3701 activatedNow.add(entry);
3702 break secondary;
3703 }
3704 }
37053712
3706 // Handle NOT ANY logic3713 // Cache the text to scan before the loop, it won't change its content
3707 if (selectiveLogic === world_info_logic.NOT_ANY && !hasAnyMatch) {3714 const textToScan = buffer.get(entry, scanState);
3708 console.debug(`(NOT ANY Check) Activating WI Entry ${entry.uid}, no secondary keywords found.`);
3709 activatedNow.add(entry);
3710 }
37113715
3712 // Handle AND ALL logic3716 // PRIMARY KEYWORDS
3713 if (selectiveLogic === world_info_logic.AND_ALL && hasAllMatch) {3717 let primaryKeyMatch = entry.key.find(key => {
3714 console.debug(`(AND ALL Check) Activating WI Entry ${entry.uid}, all secondary keywords found.`);3718 const substituted = substituteParams(key);
3715 activatedNow.add(entry);3719 return substituted && buffer.matchKeys(textToScan, substituted.trim(), entry);
3716 }3720 });
3717 } else {3721
3718 // Handle cases where secondary is empty3722 if (!primaryKeyMatch) {
3719 console.debug(`WI UID ${entry.uid}: Activated without filter logic.`);3723 // Don't write logs for simple no-matches
3720 activatedNow.add(entry);3724 continue;
3721 break primary;3725 }
3722 }3726
3727 const hasSecondaryKeywords = (
3728 entry.selective && //all entries are selective now
3729 Array.isArray(entry.keysecondary) && //always true
3730 entry.keysecondary.length //ignore empties
3731 );
3732
3733 if (!hasSecondaryKeywords) {
3734 // Handle cases where secondary is empty
3735 log('activated by primary key match', primaryKeyMatch);
3736 activatedNow.add(entry);
3737 continue;
3738 }
3739
3740
3741 // SECONDARY KEYWORDS
3742 const selectiveLogic = entry.selectiveLogic ?? 0; // If selectiveLogic isn't found, assume it's AND, only do this once per entry
3743 log('Entry with primary key match', primaryKeyMatch, 'has secondary keywords. Checking with logic logic', Object.entries(world_info_logic).find(x => x[1] === entry.selectiveLogic));
3744
3745 /** @type {() => boolean} */
3746 function matchSecondaryKeys() {
3747 let hasAnyMatch = false;
3748 let hasAllMatch = true;
3749 for (let keysecondary of entry.keysecondary) {
3750 const secondarySubstituted = substituteParams(keysecondary);
3751 const hasSecondaryMatch = secondarySubstituted && buffer.matchKeys(textToScan, secondarySubstituted.trim(), entry);
3752
3753 if (hasSecondaryMatch) hasAnyMatch = true;
3754 if (!hasSecondaryMatch) hasAllMatch = false;
3755
3756 // Simplified AND ANY / NOT ALL if statement. (Proper fix for PR#1356 by Bronya)
3757 // If AND ANY logic and the main checks pass OR if NOT ALL logic and the main checks do not pass
3758 if (selectiveLogic === world_info_logic.AND_ANY && hasSecondaryMatch) {
3759 log('activated. (AND ANY) Found match secondary keyword', secondarySubstituted);
3760 return true;
3723 }3761 }
3762 if (selectiveLogic === world_info_logic.NOT_ALL && !hasSecondaryMatch) {
3763 log('activated. (NOT ALL) Found not matching secondary keyword', secondarySubstituted);
3764 return true;
3765 }
3766 }
3767
3768 // Handle NOT ANY logic
3769 if (selectiveLogic === world_info_logic.NOT_ANY && !hasAnyMatch) {
3770 log('activated. (NOT ANY) No secondary keywords found', entry.keysecondary);
3771 return true;
3772 }
3773
3774 // Handle AND ALL logic
3775 if (selectiveLogic === world_info_logic.AND_ALL && hasAllMatch) {
3776 log('activated. (AND ALL) All secondary keywords found', entry.keysecondary);
3777 return true;
3724 }3778 }
3779
3780 return false;
3725 }3781 }
3782
3783 const matched = matchSecondaryKeys();
3784 if (!matched) {
3785 log('skipped. Secondary keywords not satisfied', entry.keysecondary);
3786 continue;
3787 }
3788
3789 // Success logging was already done inside the function, so just add the entry
3790 activatedNow.add(entry);
3791 continue;
3726 }3792 }
37273793
3728 scanState = world_info_recursive && activatedNow.size > 0 ? scan_state.RECURSION : scan_state.NONE;3794 console.debug(`[WI] Search done. Found ${activatedNow.size} possible entries.`);
3729 const newEntries = [...activatedNow]3795 const newEntries = [...activatedNow]
3730 .sort((a, b) => sortedEntries.indexOf(a) - sortedEntries.indexOf(b));3796 .sort((a, b) => sortedEntries.indexOf(a) - sortedEntries.indexOf(b));
3731 let newContent = '';3797 let newContent = '';
3732 const textToScanTokens = await getTokenCountAsync(allActivatedText);3798 const textToScanTokens = await getTokenCountAsync(allActivatedText);
3733 const probabilityChecksBefore = failedProbabilityChecks.size;
37343799
3735 filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanState);3800 filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanState);
37363801
3737 console.debug('-- PROBABILITY CHECKS BEGIN --');3802 console.debug('[WI] --- PROBABILITY CHECKS ---');
3738 for (const entry of newEntries) {3803 for (const entry of newEntries) {
3739 const rollValue = Math.random() * 100;3804 function verifyProbability() {
3805 // If we don't need to roll, it's always true
3806 if (!entry.useProbability || entry.probability === 100) {
3807 console.debug(`WI entry ${entry.uid} does not use probability`);
3808 return true;
3809 }
37403810
3741 if (entry.useProbability && rollValue > entry.probability) {
3742 const isSticky = timedEffects.isEffectActive('sticky', entry);3811 const isSticky = timedEffects.isEffectActive('sticky', entry);
3743 if (!isSticky) {3812 if (isSticky) {
3744 console.debug(`WI entry ${entry.uid} ${entry.key} failed probability check, skipping`);3813 console.debug(`WI entry ${entry.uid} is sticky, does not need to re-roll probability`);
3745 failedProbabilityChecks.add(entry);3814 return true;
3746 continue;3815 }
3816
3817 const rollValue = Math.random() * 100;
3818 if (rollValue <= entry.probability) {
3819 console.debug(`WI entry ${entry.uid} passed probability check of ${entry.probability}%`);
3820 return true;
3747 }3821 }
3748 } else { console.debug(`uid:${entry.uid} passed probability check, inserting to prompt`); }3822
3823 failedProbabilityChecks.add(entry);
3824 return false;
3825 }
3826
3827 const success = verifyProbability();
3828 if (!success) {
3829 console.debug(`WI entry ${entry.uid} failed probability check, removing from activated entries`, entry);
3830 continue;
3831 }
37493832
3750 // Substitute macros inline, for both this checking and also future processing3833 // Substitute macros inline, for both this checking and also future processing
3751 entry.content = substituteParams(entry.content);3834 entry.content = substituteParams(entry.content);
3752 newContent += `${entry.content}\n`;3835 newContent += `${entry.content}\n`;
37533836
3754 if ((textToScanTokens + (await getTokenCountAsync(newContent))) >= budget) {3837 if ((textToScanTokens + (await getTokenCountAsync(newContent))) >= budget) {
3755 console.debug('WI budget reached, stopping');
3756 if (world_info_overflow_alert) {3838 if (world_info_overflow_alert) {
3757 console.log('Alerting');3839 console.warn(`[WI] budget of ${budget} reached, stopping after ${allActivatedEntries.size} entries`);
3758 toastr.warning(`World info budget reached after ${allActivatedEntries.size} entries.`, 'World Info');3840 toastr.warning(`World info budget reached after ${allActivatedEntries.size} entries.`, 'World Info');
3841 } else {
3842 console.debug(`[WI] budget of ${budget} reached, stopping after ${allActivatedEntries.size} entries`);
3759 }3843 }
3760 scanState = scan_state.NONE;
3761 token_budget_overflowed = true;3844 token_budget_overflowed = true;
3762 break;3845 break;
3763 }3846 }
37643847
3765 allActivatedEntries.add(entry);3848 allActivatedEntries.add(entry);
3766 console.debug('WI entry activated:', entry);3849 console.debug(`[WI] Entry ${entry.uid} activation successful, adding to prompt`, entry);
3767 }3850 }
37683851
3769 const probabilityChecksAfter = failedProbabilityChecks.size;3852 const successfulNewEntries = newEntries.filter(x => !failedProbabilityChecks.has(x));
3853 const successfulNewEntriesForRecursion = successfulNewEntries.filter(x => !x.preventRecursion);
37703854
3771 if ((probabilityChecksAfter - probabilityChecksBefore) === activatedNow.size) {3855 if (!newEntries.length) {
3772 console.debug('WI probability checks failed for all activated entries, stopping');3856 console.debug('[WI] No new entries activated, stopping');
3773 scanState = scan_state.NONE;3857 } else if (!successfulNewEntries.length) {
3858 console.debug('[WI] Probability checks failed for all activated entries, stopping');
3859 } else {
3860 console.debug(`[WI] Successfully activated ${successfulNewEntries.length} new entries to prompt. ${allActivatedEntries.size} total entries activated.`, successfulNewEntries);
3774 }3861 }
37753862
3776 if (newEntries.length === 0) {3863 // After processing and rolling entries is done, see if we should continue with normal recursion
3777 console.debug('No new entries activated, stopping');3864 if (world_info_recursive && !token_budget_overflowed && successfulNewEntriesForRecursion.length) {
3778 scanState = scan_state.NONE;3865 nextScanState = scan_state.RECURSION;
3779 }3866 }
37803867
3781 if (scanState) {3868 // If we are inside min activations scan, and we have recursive buffer, we should do a recursive scan before increasing the buffer again
3782 const text = newEntries3869 // There might be recurse-trigger-able entries that match the buffer, so we need to check that
3783 .filter(x => !failedProbabilityChecks.has(x))3870 if (world_info_recursive && !token_budget_overflowed && scanState === scan_state.MIN_ACTIVATIONS && buffer.hasRecurse()) {
3784 .filter(x => !x.preventRecursion)3871 nextScanState = scan_state.RECURSION;
3785 .map(x => x.content).join('\n');
3786 buffer.addRecurse(text);
3787 allActivatedText = (text + '\n' + allActivatedText);
3788 }3872 }
37893873
3790 // world_info_min_activations3874 // If scanning is planned to stop, but min activations is set and not satisfied, check if we should continue
3791 if (!scanState && !token_budget_overflowed) {3875 const minActivationsNotSatisfied = world_info_min_activations > 0 && (allActivatedEntries.size < world_info_min_activations);
3792 if (world_info_min_activations > 0 && (allActivatedEntries.size < world_info_min_activations)) {3876 if (!nextScanState && !token_budget_overflowed && minActivationsNotSatisfied) {
3793 let over_max = (3877 console.debug('[WI] --- MIN ACTIVATIONS CHECK ---');
3794 world_info_min_activations_depth_max > 0 &&
3795 buffer.getDepth() > world_info_min_activations_depth_max
3796 ) || (buffer.getDepth() > chat.length);
37973878
3798 if (!over_max) {3879 let over_max = (
3799 scanState = scan_state.MIN_ACTIVATIONS; // loop3880 world_info_min_activations_depth_max > 0 &&
3800 buffer.advanceScanPosition();3881 buffer.getDepth() > world_info_min_activations_depth_max
3801 }3882 ) || (buffer.getDepth() > chat.length);
3883
3884 if (!over_max) {
3885 console.debug(`[WI] Min activations not reached (${allActivatedEntries.size}/${world_info_min_activations}), advancing depth to ${buffer.getDepth() + 1} and checking again`);
3886 nextScanState = scan_state.MIN_ACTIVATIONS; // loop
3887 buffer.advanceScan();
3888 } else {
3889 console.debug(`[WI] Min activations not reached (${allActivatedEntries.size}/${world_info_min_activations}), but reached on of depth. Stopping`);
3802 }3890 }
3803 }3891 }
3892
3893 // Final check if we should really continue scan, and extend the current WI recurse buffer
3894 scanState = nextScanState;
3895 if (scanState) {
3896 const text = successfulNewEntriesForRecursion
3897 .map(x => x.content).join('\n');
3898 buffer.addRecurse(text);
3899 allActivatedText = (text + '\n' + allActivatedText);
3900 }
3804 }3901 }
38053902
3903 console.debug('[WI] --- BUILDING PROMPT ---');
3904
3806 // Forward-sorted list of entries for joining3905 // Forward-sorted list of entries for joining
3807 const WIBeforeEntries = [];3906 const WIBeforeEntries = [];
3808 const WIAfterEntries = [];3907 const WIAfterEntries = [];
@@ -3818,7 +3917,7 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
3818 const content = getRegexedString(entry.content, regex_placement.WORLD_INFO, { depth: regexDepth, isMarkdown: false, isPrompt: true });3917 const content = getRegexedString(entry.content, regex_placement.WORLD_INFO, { depth: regexDepth, isMarkdown: false, isPrompt: true });
38193918
3820 if (!content) {3919 if (!content) {
3821 console.debug('Skipping adding WI entry to prompt due to empty content:', entry);3920 console.debug(`[WI] Entry ${entry.uid}`, 'skipped adding to prompt due to empty content', entry);
3822 return;3921 return;
3823 }3922 }
38243923
@@ -3876,6 +3975,9 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
3876 buffer.resetExternalEffects();3975 buffer.resetExternalEffects();
3877 timedEffects.cleanUp();3976 timedEffects.cleanUp();
38783977
3978 console.log(`[WI] Adding ${allActivatedEntries.size} entries to prompt`, Array.from(allActivatedEntries));
3979 console.debug('[WI] --- DONE ---');
3980
3879 return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, allActivatedEntries };3981 return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, allActivatedEntries };
3880}3982}
38813983
@@ -3890,13 +3992,13 @@ function filterGroupsByScoring(groups, buffer, removeEntry, scanState) {
3890 for (const [key, group] of Object.entries(groups)) {3992 for (const [key, group] of Object.entries(groups)) {
3891 // Group scoring is disabled both globally and for the group entries3993 // Group scoring is disabled both globally and for the group entries
3892 if (!world_info_use_group_scoring && !group.some(x => x.useGroupScoring)) {3994 if (!world_info_use_group_scoring && !group.some(x => x.useGroupScoring)) {
3893 console.debug(`Skipping group scoring for group '${key}'`);3995 console.debug(`[WI] Skipping group scoring for group '${key}'`);
3894 continue;3996 continue;
3895 }3997 }
38963998
3897 const scores = group.map(entry => buffer.getScore(entry, scanState));3999 const scores = group.map(entry => buffer.getScore(entry, scanState));
3898 const maxScore = Math.max(...scores);4000 const maxScore = Math.max(...scores);
3899 console.debug(`Group '${key}' max score: ${maxScore}`);4001 console.debug(`[WI] Group '${key}' max score:`, maxScore);
3900 //console.table(group.map((entry, i) => ({ uid: entry.uid, key: JSON.stringify(entry.key), score: scores[i] })));4002 //console.table(group.map((entry, i) => ({ uid: entry.uid, key: JSON.stringify(entry.key), score: scores[i] })));
39014003
3902 for (let i = 0; i < group.length; i++) {4004 for (let i = 0; i < group.length; i++) {
@@ -3907,7 +4009,7 @@ function filterGroupsByScoring(groups, buffer, removeEntry, scanState) {
3907 }4009 }
39084010
3909 if (scores[i] < maxScore) {4011 if (scores[i] < maxScore) {
3910 console.debug(`Removing score loser from inclusion group '${key}' entry '${group[i].uid}'`, group[i]);4012 console.debug(`[WI] Entry ${group[i].uid}`, `removed as score loser from inclusion group '${key}'`, group[i]);
3911 removeEntry(group[i]);4013 removeEntry(group[i]);
3912 group.splice(i, 1);4014 group.splice(i, 1);
3913 scores.splice(i, 1);4015 scores.splice(i, 1);
@@ -3925,7 +4027,8 @@ function filterGroupsByScoring(groups, buffer, removeEntry, scanState) {
3925 * @param {number} scanState The current scan state4027 * @param {number} scanState The current scan state
3926 */4028 */
3927function filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanState) {4029function filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanState) {
3928 console.debug('-- INCLUSION GROUP CHECKS BEGIN --');4030 console.debug('[WI] --- INCLUSION GROUP CHECKS ---');
4031
3929 const grouped = newEntries.filter(x => x.group).reduce((acc, item) => {4032 const grouped = newEntries.filter(x => x.group).reduce((acc, item) => {
3930 item.group.split(/,\s*/).filter(x => x).forEach(group => {4033 item.group.split(/,\s*/).filter(x => x).forEach(group => {
3931 if (!acc[group]) {4034 if (!acc[group]) {
@@ -3937,7 +4040,7 @@ function filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanSt
3937 }, {});4040 }, {});
39384041
3939 if (Object.keys(grouped).length === 0) {4042 if (Object.keys(grouped).length === 0) {
3940 console.debug('No inclusion groups found');4043 console.debug('[WI] No inclusion groups found');
3941 return;4044 return;
3942 }4045 }
39434046
@@ -3948,7 +4051,7 @@ function filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanSt
3948 continue;4051 continue;
3949 }4052 }
39504053
3951 if (logging) console.debug(`Removing loser from inclusion group '${entry.group}' entry '${entry.uid}'`, entry);4054 if (logging) console.debug(`[WI] Entry ${entry.uid}`, `removed as loser from inclusion group '${entry.group}'`, entry);
3952 removeEntry(entry);4055 removeEntry(entry);
3953 }4056 }
3954 }4057 }
@@ -3956,24 +4059,24 @@ function filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanSt
3956 filterGroupsByScoring(grouped, buffer, removeEntry, scanState);4059 filterGroupsByScoring(grouped, buffer, removeEntry, scanState);
39574060
3958 for (const [key, group] of Object.entries(grouped)) {4061 for (const [key, group] of Object.entries(grouped)) {
3959 console.debug(`Checking inclusion group '${key}' with ${group.length} entries`, group);4062 console.debug(`[WI] Checking inclusion group '${key}' with ${group.length} entries`, group);
39604063
3961 if (Array.from(allActivatedEntries).some(x => x.group === key)) {4064 if (Array.from(allActivatedEntries).some(x => x.group === key)) {
3962 console.debug(`Skipping inclusion group check, group already activated '${key}'`);4065 console.debug(`[WI] Skipping inclusion group check, group '${key}' was already activated`);
3963 // We need to forcefully deactivate all other entries in the group4066 // We need to forcefully deactivate all other entries in the group
3964 removeAllBut(group, null, false);4067 removeAllBut(group, null, false);
3965 continue;4068 continue;
3966 }4069 }
39674070
3968 if (!Array.isArray(group) || group.length <= 1) {4071 if (!Array.isArray(group) || group.length <= 1) {
3969 console.debug('Skipping inclusion group check, only one entry');4072 console.debug('[WI] Skipping inclusion group check, only one entry');
3970 continue;4073 continue;
3971 }4074 }
39724075
3973 // Check for group prio4076 // Check for group prio
3974 const prios = group.filter(x => x.groupOverride).sort(sortFn);4077 const prios = group.filter(x => x.groupOverride).sort(sortFn);
3975 if (prios.length) {4078 if (prios.length) {
3976 console.debug(`Activated inclusion group '${key}' with by prio winner entry '${prios[0].uid}'`, prios[0]);4079 console.debug(`[WI] Entry ${prios[0].uid}`, `activated as prio winner from inclusion group '${key}'`, prios[0]);
3977 removeAllBut(group, prios[0]);4080 removeAllBut(group, prios[0]);
3978 continue;4081 continue;
3979 }4082 }
@@ -3988,14 +4091,14 @@ function filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanSt
3988 currentWeight += (entry.groupWeight ?? DEFAULT_WEIGHT);4091 currentWeight += (entry.groupWeight ?? DEFAULT_WEIGHT);
39894092
3990 if (rollValue <= currentWeight) {4093 if (rollValue <= currentWeight) {
3991 console.debug(`Activated inclusion group '${key}' with roll winner entry '${entry.uid}'`, entry);4094 console.debug(`[WI] Entry ${entry.uid}`, `activated as roll winner from inclusion group '${key}'`, entry);
3992 winner = entry;4095 winner = entry;
3993 break;4096 break;
3994 }4097 }
3995 }4098 }
39964099
3997 if (!winner) {4100 if (!winner) {
3998 console.debug(`Failed to activate inclusion group '${key}', no winner found`);4101 console.debug(`[WI] Failed to activate inclusion group '${key}', no winner found`);
3999 continue;4102 continue;
4000 }4103 }
40014104