Add generation type triggers to world info (#4286) * Add generation type triggers to world info * Simplify includes check * Refactor getEntryField validation and default value handling * Remove invalid attribute * Check for a valid trigger
Signed| @@ -286,6 +286,13 @@ select.keyselect+span.select2-container .select2-selection--multiple { | ||
| 286 | 286 | display: none; |
| 287 | 287 | } |
| 288 | 288 | |
| 289 | +.world_entry label[for="__invisible"] { | |
| 290 | + visibility: hidden; | |
| 291 | + pointer-events: none; | |
| 292 | + opacity: 0; | |
| 293 | + width: 0; | |
| 294 | +} | |
| 295 | + | |
| 289 | 296 | #WIMultiSelector .select2-container .select2-selection--multiple { |
| 290 | 297 | max-height: 25vh; |
| 291 | 298 | overflow-y: auto; |
| @@ -6494,6 +6494,30 @@ | ||
| 6494 | 6494 | </select> |
| 6495 | 6495 | </div> |
| 6496 | 6496 | </div> |
| 6497 | + <div class="flex4"> | |
| 6498 | + <div class="flex-container justifySpaceBetween"> | |
| 6499 | + <small> | |
| 6500 | + <span data-i18n="Filter to Generation Triggers"> | |
| 6501 | + Filter to Generation Triggers | |
| 6502 | + </span> | |
| 6503 | + </small> | |
| 6504 | + <!-- Not a real control. Used to make label heights even. --> | |
| 6505 | + <label class="checkbox_label" for="__invisible"> | |
| 6506 | + <input type="checkbox" name="__invisible"> | |
| 6507 | + <span><small> </small></span> | |
| 6508 | + </label> | |
| 6509 | + </div> | |
| 6510 | + <div class="range-block-range"> | |
| 6511 | + <select name="triggers" multiple> | |
| 6512 | + <option data-i18n="Normal" value="normal">Normal</option> | |
| 6513 | + <option data-i18n="Continue" value="continue">Continue</option> | |
| 6514 | + <option data-i18n="Impersonate" value="impersonate">Impersonate</option> | |
| 6515 | + <option data-i18n="Swipe" value="swipe">Swipe</option> | |
| 6516 | + <option data-i18n="Regenerate" value="regenerate">Regenerate</option> | |
| 6517 | + <option data-i18n="Quiet" value="quiet">Quiet</option> | |
| 6518 | + </select> | |
| 6519 | + </div> | |
| 6520 | + </div> | |
| 6497 | 6521 | </div> |
| 6498 | 6522 | <div name="WIEntryBottomControls" class="flex-container flex1 justifySpaceBetween world_entry_form_horizontal"> |
| 6499 | 6523 | <div class="flex-container flexFlowColumn flexNoGap wi-enter-footer-text"> |
| @@ -176,7 +176,7 @@ import { | ||
| 176 | 176 | renderPaginationDropdown, |
| 177 | 177 | paginationDropdownChangeHandler, |
| 178 | 178 | } from './scripts/utils.js'; |
| 179 | 179 | import { debounce_timeout, GENERATION_TYPE_TRIGGERS, IGNORE_SYMBOL } from './scripts/constants.js'; |
| 180 | 180 | |
| 181 | 181 | import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors, saveMetadataDebounced } from './scripts/extensions.js'; |
| 182 | 182 | import { COMMENT_NAME_DEFAULT, CONNECT_API_MAP, executeSlashCommandsOnChatInput, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, stopScriptExecution, UNIQUE_APIS } from './scripts/slash-commands.js'; |
| @@ -3721,6 +3721,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 3721 | 3721 | // Make quiet prompt available for WIAN |
| 3722 | 3722 | setExtensionPrompt('QUIET_PROMPT', quiet_prompt || '', extension_prompt_types.IN_PROMPT, 0, true); |
| 3723 | 3723 | const chatForWI = coreChat.map(x => world_info_include_names ? `${x.name}: ${x.mes}` : x.mes).reverse(); |
| 3724 | + /** @type {import('./scripts/world-info.js').WIGlobalScanData} */ | |
| 3724 | 3725 | const globalScanData = { |
| 3725 | 3726 | personaDescription: persona, |
| 3726 | 3727 | characterDescription: description, |
| @@ -3728,6 +3729,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 3728 | 3729 | characterDepthPrompt: charDepthPrompt, |
| 3729 | 3730 | scenario: scenario, |
| 3730 | 3731 | creatorNotes: creatorNotes, |
| 3732 | + trigger: GENERATION_TYPE_TRIGGERS.includes(type) ? type : 'normal', | |
| 3731 | 3733 | }; |
| 3732 | 3734 | const { worldInfoString, worldInfoBefore, worldInfoAfter, worldInfoExamples, worldInfoDepth } = await getWorldInfoPrompt(chatForWI, this_max_context, dryRun, globalScanData); |
| 3733 | 3735 | setExtensionPrompt('QUIET_PROMPT', '', extension_prompt_types.IN_PROMPT, 0, true); |
| @@ -28,3 +28,15 @@ export const IGNORE_SYMBOL = Symbol.for('ignore'); | ||
| 28 | 28 | * https://ai.google.dev/gemini-api/docs/video-understanding#supported-formats |
| 29 | 29 | */ |
| 30 | 30 | export const VIDEO_EXTENSIONS = ['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', '3gp', 'mkv', 'mpg']; |
| 31 | + | |
| 32 | +/** | |
| 33 | + * Known generation triggers that can be passed to Generate function. | |
| 34 | + */ | |
| 35 | +export const GENERATION_TYPE_TRIGGERS = [ | |
| 36 | + 'normal', | |
| 37 | + 'continue', | |
| 38 | + 'impersonate', | |
| 39 | + 'swipe', | |
| 40 | + 'regenerate', | |
| 41 | + 'quiet', | |
| 42 | +]; | |
| @@ -9,7 +9,7 @@ import { FILTER_TYPES, FilterHelper } from './filters.js'; | ||
| 9 | 9 | import { getTokenCountAsync } from './tokenizers.js'; |
| 10 | 10 | import { power_user } from './power-user.js'; |
| 11 | 11 | import { getTagKeyForEntity } from './tags.js'; |
| 12 | 12 | import { debounce_timeout, GENERATION_TYPE_TRIGGERS } from './constants.js'; |
| 13 | 13 | import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; |
| 14 | 14 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 15 | 15 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| @@ -107,6 +107,7 @@ const KNOWN_DECORATORS = ['@@activate', '@@dont_activate']; | ||
| 107 | 107 | * @property {string} characterDepthPrompt Character depth prompt (sometimes referred to as character notes) |
| 108 | 108 | * @property {string} scenario Character defined scenario |
| 109 | 109 | * @property {string} creatorNotes Character creator notes |
| 110 | + * @property {string} trigger The type that triggered the scan, e.g. 'normal', 'continue', etc. | |
| 110 | 111 | */ |
| 111 | 112 | |
| 112 | 113 | /** |
| @@ -145,6 +146,36 @@ const KNOWN_DECORATORS = ['@@activate', '@@dont_activate']; | ||
| 145 | 146 | * @typedef TimedEffectType Type of timed effect |
| 146 | 147 | * @type {'sticky'|'cooldown'|'delay'} |
| 147 | 148 | */ |
| 149 | + | |
| 150 | +/** | |
| 151 | + * @typedef {object} WIPromptResult | |
| 152 | + * @property {string} worldInfoString - Complete world info string | |
| 153 | + * @property {string} worldInfoBefore - World info that goes before the prompt | |
| 154 | + * @property {string} worldInfoAfter - World info that goes after the prompt | |
| 155 | + * @property {Array} worldInfoExamples - Array of example entries | |
| 156 | + * @property {Array} worldInfoDepth - Array of depth entries | |
| 157 | + * @property {Array} anBefore - Array of entries before Author's Note | |
| 158 | + * @property {Array} anAfter - Array of entries after Author's Note | |
| 159 | + */ | |
| 160 | + | |
| 161 | +/** | |
| 162 | + * @typedef {object} WIActivated | |
| 163 | + * @property {string} worldInfoBefore The world info before the chat. | |
| 164 | + * @property {string} worldInfoAfter The world info after the chat. | |
| 165 | + * @property {any[]} EMEntries The entries for examples. | |
| 166 | + * @property {any[]} WIDepthEntries The depth entries. | |
| 167 | + * @property {any[]} ANBeforeEntries The entries before Author's Note. | |
| 168 | + * @property {any[]} ANAfterEntries The entries after Author's Note. | |
| 169 | + * @property {Set<any>} allActivatedEntries All entries. | |
| 170 | + */ | |
| 171 | + | |
| 172 | +/** | |
| 173 | + * @typedef {object} WIEntryFieldDefinition | |
| 174 | + * @property {any} default - Default value for the field | |
| 175 | + * @property {string} type - Type of the field, can be 'string', 'number', 'boolean', 'array', 'enum' | |
| 176 | + * @property {boolean} [excludeFromTemplate=false] - Whether to exclude this field from the template | |
| 177 | + * @property {(value: any) => boolean} [arrayFilter] - Optional filter function for array fields to filter out unwanted values | |
| 178 | + */ | |
| 148 | 179 | // End typedef area |
| 149 | 180 | |
| 150 | 181 | /** |
| @@ -801,14 +832,6 @@ export const worldInfoCache = new StructuredCloneMap({ cloneOnGet: true, cloneOn | ||
| 801 | 832 | * @param {number} maxContext - The maximum context size of the generation. |
| 802 | 833 | * @param {boolean} isDryRun - If true, the function will not emit any events. |
| 803 | 834 | * @param {WIGlobalScanData} globalScanData Chat independent context to be scanned |
| 804 | - * @typedef {object} WIPromptResult | |
| 805 | - * @property {string} worldInfoString - Complete world info string | |
| 806 | - * @property {string} worldInfoBefore - World info that goes before the prompt | |
| 807 | - * @property {string} worldInfoAfter - World info that goes after the prompt | |
| 808 | - * @property {Array} worldInfoExamples - Array of example entries | |
| 809 | - * @property {Array} worldInfoDepth - Array of depth entries | |
| 810 | - * @property {Array} anBefore - Array of entries before Author's Note | |
| 811 | - * @property {Array} anAfter - Array of entries after Author's Note | |
| 812 | 835 | * @returns {Promise<WIPromptResult>} The world info string and depth. |
| 813 | 836 | */ |
| 814 | 837 | export async function getWorldInfoPrompt(chat, maxContext, isDryRun, globalScanData) { |
| @@ -1139,7 +1162,7 @@ function registerWorldInfoSlashCommands() { | ||
| 1139 | 1162 | return ''; |
| 1140 | 1163 | } |
| 1141 | 1164 | |
| 1142 | - if (newWorldInfoEntryTemplate[field] === undefined) { | |
| 1165 | + if (!Object.hasOwn(newWorldInfoEntryDefinition, field)) { | |
| 1143 | 1166 | toastr.warning('Valid field name is required'); |
| 1144 | 1167 | return ''; |
| 1145 | 1168 | } |
| @@ -1167,7 +1190,7 @@ function registerWorldInfoSlashCommands() { | ||
| 1167 | 1190 | } |
| 1168 | 1191 | break; |
| 1169 | 1192 | default: |
| 1170 | 1193 | fieldValue = entry[field] ?? newWorldInfoEntryDefinition[field]?.default; |
| 1171 | 1194 | } |
| 1172 | 1195 | |
| 1173 | 1196 | if (fieldValue === undefined) { |
| @@ -1253,11 +1276,19 @@ function registerWorldInfoSlashCommands() { | ||
| 1253 | 1276 | return ''; |
| 1254 | 1277 | } |
| 1255 | 1278 | |
| 1256 | - if (newWorldInfoEntryTemplate[field] === undefined) { | |
| 1279 | + if (!Object.hasOwn(newWorldInfoEntryDefinition, field)) { | |
| 1257 | 1280 | toastr.warning('Valid field name is required'); |
| 1258 | 1281 | return ''; |
| 1259 | 1282 | } |
| 1260 | 1283 | |
| 1284 | + // Init a default value for the field if it does not exist | |
| 1285 | + if (!Object.hasOwn(entry, field)) { | |
| 1286 | + entry[field] = newWorldInfoEntryDefinition[field].default; | |
| 1287 | + } | |
| 1288 | + | |
| 1289 | + // Use an array filter if it exists for the field | |
| 1290 | + const arrayFilter = newWorldInfoEntryDefinition[field]?.arrayFilter || (() => true); | |
| 1291 | + | |
| 1261 | 1292 | // handle special cases, otherwise execute default logic |
| 1262 | 1293 | let tagNames; |
| 1263 | 1294 | let charNames; |
| @@ -1285,7 +1316,7 @@ function registerWorldInfoSlashCommands() { | ||
| 1285 | 1316 | break; |
| 1286 | 1317 | default: |
| 1287 | 1318 | if (Array.isArray(entry[field])) { |
| 1288 | 1319 | entry[field] = parseStringArray(value).filter(arrayFilter); |
| 1289 | 1320 | } else if (typeof entry[field] === 'boolean') { |
| 1290 | 1321 | entry[field] = isTrueBoolean(value); |
| 1291 | 1322 | } else if (typeof entry[field] === 'number') { |
| @@ -2438,6 +2469,7 @@ export const originalWIDataKeyMap = { | ||
| 2438 | 2469 | 'sticky': 'extensions.sticky', |
| 2439 | 2470 | 'cooldown': 'extensions.cooldown', |
| 2440 | 2471 | 'delay': 'extensions.delay', |
| 2472 | + 'triggers': 'extensions.triggers', | |
| 2441 | 2473 | }; |
| 2442 | 2474 | |
| 2443 | 2475 | /** Checks the state of the current search, and adds/removes the search sorting option accordingly */ |
| @@ -3496,6 +3528,29 @@ export async function getWorldEntry(name, data, entry) { | ||
| 3496 | 3528 | automationIdInput.val(entry.automationId ?? '').trigger('input', { noSave: true }); |
| 3497 | 3529 | setTimeout(() => createEntryInputAutocomplete(automationIdInput, getAutomationIdCallback(data)), 1); |
| 3498 | 3530 | |
| 3531 | + // Generation Type Triggers | |
| 3532 | + const generationTypeTriggers = editTemplate.find('select[name="triggers"]'); | |
| 3533 | + generationTypeTriggers.data('uid', entry.uid); | |
| 3534 | + generationTypeTriggers.on('input', async function (_, { noSave = false } = {}) { | |
| 3535 | + const uid = $(this).data('uid'); | |
| 3536 | + const value = $(this).val(); | |
| 3537 | + data.entries[uid].triggers = Array.isArray(value) ? value : []; | |
| 3538 | + setWIOriginalDataValue(data, uid, 'extensions.triggers', data.entries[uid].triggers); | |
| 3539 | + !noSave && await saveWorldInfo(name, data); | |
| 3540 | + }); | |
| 3541 | + if (!isMobile()) { | |
| 3542 | + generationTypeTriggers.select2({ | |
| 3543 | + placeholder: t`All types (default)`, | |
| 3544 | + width: '100%', | |
| 3545 | + closeOnSelect: false, | |
| 3546 | + allowClear: true, | |
| 3547 | + }); | |
| 3548 | + } | |
| 3549 | + generationTypeTriggers | |
| 3550 | + .val(Array.isArray(entry.triggers) ? entry.triggers : []) | |
| 3551 | + .trigger('input', { noSave: true }) | |
| 3552 | + .trigger('change'); | |
| 3553 | + | |
| 3499 | 3554 | countTokensDebounced(counter, contentInput.val()); |
| 3500 | 3555 | |
| 3501 | 3556 | editTemplate.find('.inline-drawer-content').css('display', 'none'); |
| @@ -3652,7 +3707,7 @@ export async function deleteWorldInfoEntry(data, uid, { silent = false } = {}) { | ||
| 3652 | 3707 | * |
| 3653 | 3708 | * Use `newEntryTemplate` if you just need the template that contains default values |
| 3654 | 3709 | * |
| 3655 | 3710 | * @type {{[key: string]: { default: any, type: string, excludeFromTemplate?: boolean }WIEntryFieldDefinition}} |
| 3656 | 3711 | */ |
| 3657 | 3712 | export const newWorldInfoEntryDefinition = { |
| 3658 | 3713 | key: { default: [], type: 'array' }, |
| @@ -3694,6 +3749,7 @@ export const newWorldInfoEntryDefinition = { | ||
| 3694 | 3749 | characterFilterNames: { default: [], type: 'array', excludeFromTemplate: true }, |
| 3695 | 3750 | characterFilterTags: { default: [], type: 'array', excludeFromTemplate: true }, |
| 3696 | 3751 | characterFilterExclude: { default: false, type: 'boolean', excludeFromTemplate: true }, |
| 3752 | + triggers: { default: [], type: 'array', arrayFilter: (value) => GENERATION_TYPE_TRIGGERS.includes(value) }, | |
| 3697 | 3753 | }; |
| 3698 | 3754 | |
| 3699 | 3755 | export const newWorldInfoEntryTemplate = Object.fromEntries( |
| @@ -4143,23 +4199,14 @@ function parseDecorators(content) { | ||
| 4143 | 4199 | * @param {number} maxContext The maximum context size of the generation. |
| 4144 | 4200 | * @param {boolean} isDryRun Whether to perform a dry run. |
| 4145 | 4201 | * @param {WIGlobalScanData} globalScanData Chat independent context to be scanned |
| 4146 | - * @typedef {object} WIActivated | |
| 4147 | - * @property {string} worldInfoBefore The world info before the chat. | |
| 4148 | - * @property {string} worldInfoAfter The world info after the chat. | |
| 4149 | - * @property {any[]} EMEntries The entries for examples. | |
| 4150 | - * @property {any[]} WIDepthEntries The depth entries. | |
| 4151 | - * @property {any[]} ANBeforeEntries The entries before Author's Note. | |
| 4152 | - * @property {any[]} ANAfterEntries The entries after Author's Note. | |
| 4153 | - * @property {Set<any>} allActivatedEntries All entries. | |
| 4154 | 4202 | * @returns {Promise<WIActivated>} The world info activated. |
| 4155 | 4203 | */ |
| 4156 | - | |
| 4157 | 4204 | //MARK: checkWorldInfo |
| 4158 | 4205 | export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData) { |
| 4159 | 4206 | const context = getContext(); |
| 4160 | 4207 | const buffer = new WorldInfoBuffer(chat, globalScanData); |
| 4161 | 4208 | |
| 4162 | 4209 | console.debug(`[WI] --- START WI SCAN (on ${chat.length} messages, trigger = ${globalScanData.trigger})${isDryRun ? ' (DRY RUN)' : ''} ---`); |
| 4163 | 4210 | |
| 4164 | 4211 | // Combine the chat |
| 4165 | 4212 | |
| @@ -4231,7 +4278,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData) | ||
| 4231 | 4278 | // Loop and find all entries that can activate here |
| 4232 | 4279 | let activatedNow = new Set(); |
| 4233 | 4280 | |
| 4234 | 4281 | for (letconst entry of sortedEntries) { |
| 4235 | 4282 | // Logging preparation |
| 4236 | 4283 | let headerLogged = false; |
| 4237 | 4284 | function log(...args) { |
| @@ -4252,6 +4299,15 @@ export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData) | ||
| 4252 | 4299 | continue; |
| 4253 | 4300 | } |
| 4254 | 4301 | |
| 4302 | + // Check for generation type trigger filter | |
| 4303 | + if (Array.isArray(entry.triggers) && entry.triggers.length > 0) { | |
| 4304 | + const isTriggered = entry.triggers.includes(globalScanData.trigger); | |
| 4305 | + if (!isTriggered) { | |
| 4306 | + log(`skipped by generation type trigger filter (${globalScanData.trigger} ∉ ${entry.triggers})`); | |
| 4307 | + continue; | |
| 4308 | + } | |
| 4309 | + } | |
| 4310 | + | |
| 4255 | 4311 | // Check if this entry applies to the character or if it's excluded |
| 4256 | 4312 | if (entry.characterFilter && entry.characterFilter?.names?.length > 0) { |
| 4257 | 4313 | const nameIncluded = entry.characterFilter.names.includes(getCharaFilename()); |
| @@ -4877,6 +4933,7 @@ function convertAgnaiMemoryBook(inputObj) { | ||
| 4877 | 4933 | sticky: null, |
| 4878 | 4934 | cooldown: null, |
| 4879 | 4935 | delay: null, |
| 4936 | + triggers: [], | |
| 4880 | 4937 | }; |
| 4881 | 4938 | }); |
| 4882 | 4939 | |
| @@ -4919,6 +4976,7 @@ function convertRisuLorebook(inputObj) { | ||
| 4919 | 4976 | sticky: null, |
| 4920 | 4977 | cooldown: null, |
| 4921 | 4978 | delay: null, |
| 4979 | + triggers: [], | |
| 4922 | 4980 | }; |
| 4923 | 4981 | }); |
| 4924 | 4982 | |
| @@ -4966,6 +5024,7 @@ function convertNovelLorebook(inputObj) { | ||
| 4966 | 5024 | sticky: null, |
| 4967 | 5025 | cooldown: null, |
| 4968 | 5026 | delay: null, |
| 5027 | + triggers: [], | |
| 4969 | 5028 | }; |
| 4970 | 5029 | }); |
| 4971 | 5030 | |
| @@ -5022,6 +5081,7 @@ export function convertCharacterBook(characterBook) { | ||
| 5022 | 5081 | matchScenario: entry.extensions?.match_scenario ?? false, |
| 5023 | 5082 | matchCreatorNotes: entry.extensions?.match_creator_notes ?? false, |
| 5024 | 5083 | extensions: entry.extensions ?? {}, |
| 5084 | + triggers: entry.extensions?.triggers || [], | |
| 5025 | 5085 | }; |
| 5026 | 5086 | }); |
| 5027 | 5087 | |
| @@ -707,6 +707,7 @@ function convertWorldInfoToCharacterBook(name, entries) { | ||
| 707 | 707 | match_character_depth_prompt: entry.matchCharacterDepthPrompt ?? false, |
| 708 | 708 | match_scenario: entry.matchScenario ?? false, |
| 709 | 709 | match_creator_notes: entry.matchCreatorNotes ?? false, |
| 710 | + triggers: entry.triggers ?? [], | |
| 710 | 711 | }, |
| 711 | 712 | }; |
| 712 | 713 | |