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