Merge pull request #2468 from SillyTavern/wi-scan-state Fix min activations for non-recursable entries
Signed| @@ -50,6 +50,28 @@ const world_info_logic = { | ||
| 50 | 50 | AND_ALL: 3, |
| 51 | 51 | }; |
| 52 | 52 | |
| 53 | +/** | |
| 54 | + * @enum {number} Possible states of the WI evaluation | |
| 55 | + */ | |
| 56 | +const scan_state = { | |
| 57 | + /** | |
| 58 | + * The scan will be stopped. | |
| 59 | + */ | |
| 60 | + NONE: 0, | |
| 61 | + /** | |
| 62 | + * Initial state. | |
| 63 | + */ | |
| 64 | + INITIAL: 1, | |
| 65 | + /** | |
| 66 | + * The scan is triggered by a recursion step. | |
| 67 | + */ | |
| 68 | + RECURSION: 2, | |
| 69 | + /** | |
| 70 | + * The scan is triggered by a min activations depth skew. | |
| 71 | + */ | |
| 72 | + MIN_ACTIVATIONS: 2, | |
| 73 | +}; | |
| 74 | + | |
| 53 | 75 | const WI_ENTRY_EDIT_TEMPLATE = $('#entry_edit_template .world_entry'); |
| 54 | 76 | |
| 55 | 77 | let world_info = {}; |
| @@ -136,6 +158,11 @@ class WorldInfoBuffer { | ||
| 136 | 158 | #recurseBuffer = []; |
| 137 | 159 | |
| 138 | 160 | /** |
| 161 | + * @type {string[]} Array of strings added by prompt injections that are valid for the current scan | |
| 162 | + */ | |
| 163 | + #injectBuffer = []; | |
| 164 | + | |
| 165 | + /** | |
| 139 | 166 | * @type {number} The skew of the global scan depth. Used in "min activations" |
| 140 | 167 | */ |
| 141 | 168 | #skew = 0; |
| @@ -184,9 +211,10 @@ class WorldInfoBuffer { | ||
| 184 | 211 | /** |
| 185 | 212 | * Gets all messages up to the given depth + recursion buffer. |
| 186 | 213 | * @param {WIScanEntry} entry The entry that triggered the scan |
| 214 | + * @param {number} scanState The state of the scan | |
| 187 | 215 | * @returns {string} A slice of buffer until the given depth (inclusive) |
| 188 | 216 | */ |
| 189 | 217 | get(entry, scanState) { |
| 190 | 218 | let depth = entry.scanDepth ?? this.getDepth(); |
| 191 | 219 | if (depth <= this.#startDepth) { |
| 192 | 220 | return ''; |
| @@ -204,7 +232,12 @@ class WorldInfoBuffer { | ||
| 204 | 232 | |
| 205 | 233 | let result = this.#depthBuffer.slice(this.#startDepth, depth).join('\n'); |
| 206 | 234 | |
| 207 | 235 | if (this.#recurseBufferinjectBuffer.length > 0) { |
| 236 | + result += '\n' + this.#injectBuffer.join('\n'); | |
| 237 | + } | |
| 238 | + | |
| 239 | + // Min activations should not include the recursion buffer | |
| 240 | + if (this.#recurseBuffer.length > 0 && scanState !== scan_state.MIN_ACTIVATIONS) { | |
| 208 | 241 | result += '\n' + this.#recurseBuffer.join('\n'); |
| 209 | 242 | } |
| 210 | 243 | |
| @@ -259,6 +292,14 @@ class WorldInfoBuffer { | ||
| 259 | 292 | } |
| 260 | 293 | |
| 261 | 294 | /** |
| 295 | + * Adds an injection to the buffer. | |
| 296 | + * @param {string} message The injection to add | |
| 297 | + */ | |
| 298 | + addInject(message) { | |
| 299 | + this.#injectBuffer.push(message); | |
| 300 | + } | |
| 301 | + | |
| 302 | + /** | |
| 262 | 303 | * Increments skew and sets startDepth to previous depth. |
| 263 | 304 | */ |
| 264 | 305 | advanceScanPosition() { |
| @@ -293,10 +334,11 @@ class WorldInfoBuffer { | ||
| 293 | 334 | /** |
| 294 | 335 | * Gets the match score for the given entry. |
| 295 | 336 | * @param {WIScanEntry} entry Entry to check |
| 337 | + * @param {number} scanState The state of the scan | |
| 296 | 338 | * @returns {number} The number of key activations for the given entry |
| 297 | 339 | */ |
| 298 | 340 | getScore(entry, scanState) { |
| 299 | 341 | const bufferState = this.get(entry, scanState); |
| 300 | 342 | let numberOfPrimaryKeys = 0; |
| 301 | 343 | let numberOfSecondaryKeys = 0; |
| 302 | 344 | let primaryScore = 0; |
| @@ -3503,12 +3545,12 @@ async function checkWorldInfo(chat, maxContext, isDryRun) { | ||
| 3503 | 3545 | if (context.extensionPrompts[key]?.scan) { |
| 3504 | 3546 | const prompt = getExtensionPromptByName(key); |
| 3505 | 3547 | if (prompt) { |
| 3506 | 3548 | buffer.addRecurseaddInject(prompt); |
| 3507 | 3549 | } |
| 3508 | 3550 | } |
| 3509 | 3551 | } |
| 3510 | 3552 | |
| 3511 | 3553 | let needsToScanscanState = truescan_state.INITIAL; |
| 3512 | 3554 | let token_budget_overflowed = false; |
| 3513 | 3555 | let count = 0; |
| 3514 | 3556 | let allActivatedEntries = new Set(); |
| @@ -3532,8 +3574,9 @@ async function checkWorldInfo(chat, maxContext, isDryRun) { | ||
| 3532 | 3574 | return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], allActivatedEntries: new Set() }; |
| 3533 | 3575 | } |
| 3534 | 3576 | |
| 3535 | 3577 | while (needsToScanscanState) { |
| 3536 | 3578 | // Track how many times the loop has run. May be useful for debugging. |
| 3579 | + // eslint-disable-next-line no-unused-vars | |
| 3537 | 3580 | count++; |
| 3538 | 3581 | |
| 3539 | 3582 | let activatedNow = new Set(); |
| @@ -3587,7 +3630,18 @@ async function checkWorldInfo(chat, maxContext, isDryRun) { | ||
| 3587 | 3630 | continue; |
| 3588 | 3631 | } |
| 3589 | 3632 | |
| 3590 | 3633 | if (allActivatedEntries.has(entry) || entry.disable == true || (count > 1 && world_info_recursive && entry.excludeRecursion) || (count == 1 && entry.delayUntilRecursion)) { |
| 3634 | + continue; | |
| 3635 | + } | |
| 3636 | + | |
| 3637 | + // Only use checks for recursion flags if the scan step was activated by recursion | |
| 3638 | + if (scanState !== scan_state.RECURSION && entry.delayUntilRecursion) { | |
| 3639 | + console.debug(`WI entry ${entry.uid} suppressed by delay until recursion`, entry); | |
| 3640 | + continue; | |
| 3641 | + } | |
| 3642 | + | |
| 3643 | + if (scanState === scan_state.RECURSION && world_info_recursive && entry.excludeRecursion) { | |
| 3644 | + console.debug(`WI entry ${entry.uid} suppressed by exclude recursion`, entry); | |
| 3591 | 3645 | continue; |
| 3592 | 3646 | } |
| 3593 | 3647 | |
| @@ -3602,7 +3656,7 @@ async function checkWorldInfo(chat, maxContext, isDryRun) { | ||
| 3602 | 3656 | |
| 3603 | 3657 | primary: for (let key of entry.key) { |
| 3604 | 3658 | const substituted = substituteParams(key); |
| 3605 | 3659 | const textToScan = buffer.get(entry, scanState); |
| 3606 | 3660 | |
| 3607 | 3661 | if (substituted && buffer.matchKeys(textToScan, substituted.trim(), entry)) { |
| 3608 | 3662 | console.debug(`WI UID ${entry.uid} found by primary match: ${substituted}.`); |
| @@ -3665,14 +3719,14 @@ async function checkWorldInfo(chat, maxContext, isDryRun) { | ||
| 3665 | 3719 | } |
| 3666 | 3720 | } |
| 3667 | 3721 | |
| 3668 | 3722 | needsToScanscanState = world_info_recursive && activatedNow.size > 0 ? scan_state.RECURSION : scan_state.NONE; |
| 3669 | 3723 | const newEntries = [...activatedNow] |
| 3670 | 3724 | .sort((a, b) => sortedEntries.indexOf(a) - sortedEntries.indexOf(b)); |
| 3671 | 3725 | let newContent = ''; |
| 3672 | 3726 | const textToScanTokens = await getTokenCountAsync(allActivatedText); |
| 3673 | 3727 | const probabilityChecksBefore = failedProbabilityChecks.size; |
| 3674 | 3728 | |
| 3675 | 3729 | filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanState); |
| 3676 | 3730 | |
| 3677 | 3731 | console.debug('-- PROBABILITY CHECKS BEGIN --'); |
| 3678 | 3732 | for (const entry of newEntries) { |
| @@ -3697,7 +3751,7 @@ async function checkWorldInfo(chat, maxContext, isDryRun) { | ||
| 3697 | 3751 | console.log('Alerting'); |
| 3698 | 3752 | toastr.warning(`World info budget reached after ${allActivatedEntries.size} entries.`, 'World Info'); |
| 3699 | 3753 | } |
| 3700 | 3754 | needsToScanscanState = falsescan_state.NONE; |
| 3701 | 3755 | token_budget_overflowed = true; |
| 3702 | 3756 | break; |
| 3703 | 3757 | } |
| @@ -3710,15 +3764,15 @@ async function checkWorldInfo(chat, maxContext, isDryRun) { | ||
| 3710 | 3764 | |
| 3711 | 3765 | if ((probabilityChecksAfter - probabilityChecksBefore) === activatedNow.size) { |
| 3712 | 3766 | console.debug('WI probability checks failed for all activated entries, stopping'); |
| 3713 | 3767 | needsToScanscanState = falsescan_state.NONE; |
| 3714 | 3768 | } |
| 3715 | 3769 | |
| 3716 | 3770 | if (newEntries.length === 0) { |
| 3717 | 3771 | console.debug('No new entries activated, stopping'); |
| 3718 | 3772 | needsToScanscanState = falsescan_state.NONE; |
| 3719 | 3773 | } |
| 3720 | 3774 | |
| 3721 | 3775 | if (needsToScanscanState) { |
| 3722 | 3776 | const text = newEntries |
| 3723 | 3777 | .filter(x => !failedProbabilityChecks.has(x)) |
| 3724 | 3778 | .filter(x => !x.preventRecursion) |
| @@ -3728,7 +3782,7 @@ async function checkWorldInfo(chat, maxContext, isDryRun) { | ||
| 3728 | 3782 | } |
| 3729 | 3783 | |
| 3730 | 3784 | // world_info_min_activations |
| 3731 | 3785 | if (!needsToScanscanState && !token_budget_overflowed) { |
| 3732 | 3786 | if (world_info_min_activations > 0 && (allActivatedEntries.size < world_info_min_activations)) { |
| 3733 | 3787 | let over_max = ( |
| 3734 | 3788 | world_info_min_activations_depth_max > 0 && |
| @@ -3736,7 +3790,7 @@ async function checkWorldInfo(chat, maxContext, isDryRun) { | ||
| 3736 | 3790 | ) || (buffer.getDepth() > chat.length); |
| 3737 | 3791 | |
| 3738 | 3792 | if (!over_max) { |
| 3739 | 3793 | needsToScanscanState = truescan_state.MIN_ACTIVATIONS; // loop |
| 3740 | 3794 | buffer.advanceScanPosition(); |
| 3741 | 3795 | } |
| 3742 | 3796 | } |
| @@ -3824,8 +3878,9 @@ async function checkWorldInfo(chat, maxContext, isDryRun) { | ||
| 3824 | 3878 | * @param {Record<string, WIScanEntry[]>} groups The groups to filter |
| 3825 | 3879 | * @param {WorldInfoBuffer} buffer The buffer to use for scoring |
| 3826 | 3880 | * @param {(entry: WIScanEntry) => void} removeEntry The function to remove an entry |
| 3881 | + * @param {number} scanState The current scan state | |
| 3827 | 3882 | */ |
| 3828 | 3883 | function filterGroupsByScoring(groups, buffer, removeEntry, scanState) { |
| 3829 | 3884 | for (const [key, group] of Object.entries(groups)) { |
| 3830 | 3885 | // Group scoring is disabled both globally and for the group entries |
| 3831 | 3886 | if (!world_info_use_group_scoring && !group.some(x => x.useGroupScoring)) { |
| @@ -3833,7 +3888,7 @@ function filterGroupsByScoring(groups, buffer, removeEntry) { | ||
| 3833 | 3888 | continue; |
| 3834 | 3889 | } |
| 3835 | 3890 | |
| 3836 | 3891 | const scores = group.map(entry => buffer.getScore(entry, scanState)); |
| 3837 | 3892 | const maxScore = Math.max(...scores); |
| 3838 | 3893 | console.debug(`Group '${key}' max score: ${maxScore}`); |
| 3839 | 3894 | //console.table(group.map((entry, i) => ({ uid: entry.uid, key: JSON.stringify(entry.key), score: scores[i] }))); |
| @@ -3861,8 +3916,9 @@ function filterGroupsByScoring(groups, buffer, removeEntry) { | ||
| 3861 | 3916 | * @param {object[]} newEntries Entries activated on current recursion level |
| 3862 | 3917 | * @param {Set<object>} allActivatedEntries Set of all activated entries |
| 3863 | 3918 | * @param {WorldInfoBuffer} buffer The buffer to use for scanning |
| 3919 | + * @param {number} scanState The current scan state | |
| 3864 | 3920 | */ |
| 3865 | 3921 | function filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanState) { |
| 3866 | 3922 | console.debug('-- INCLUSION GROUP CHECKS BEGIN --'); |
| 3867 | 3923 | const grouped = newEntries.filter(x => x.group).reduce((acc, item) => { |
| 3868 | 3924 | item.group.split(/,\s*/).filter(x => x).forEach(group => { |
| @@ -3891,7 +3947,7 @@ function filterByInclusionGroups(newEntries, allActivatedEntries, buffer) { | ||
| 3891 | 3947 | } |
| 3892 | 3948 | } |
| 3893 | 3949 | |
| 3894 | 3950 | filterGroupsByScoring(grouped, buffer, removeEntry, scanState); |
| 3895 | 3951 | |
| 3896 | 3952 | for (const [key, group] of Object.entries(grouped)) { |
| 3897 | 3953 | console.debug(`Checking inclusion group '${key}' with ${group.length} entries`, group); |