Merge branch 'staging' into tc-split-generic

756f88b5aa612f49a81a6d473cb39fe8de9d3758

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

6 files changed, +157 -52Showing whitespace changes
public/script.js+75 -33
@@ -2876,23 +2876,54 @@ function addPersonaDescriptionExtensionPrompt() {
28762876 }
28772877}
28782878
2879-function getAllExtensionPrompts() {
2879+/**
2880- const value = Object
2880+ * Returns all extension prompts combined.
2881- .values(extension_prompts)
2881+ * @returns {Promise<string>} Combined extension prompts
2882- .filter(x => x.value)
2882+ */
2883- .map(x => x.value.trim())
2883+async function getAllExtensionPrompts() {
2884- .join('\n');
2884+ const values = [];
28852885
2886- return value.length ? substituteParams(value) : '';
2886+ for (const prompt of Object.values(extension_prompts)) {
2887+ const value = prompt?.value?.trim();
2888+
2889+ if (!value) {
2890+ continue;
28872891 }
28882892
2889-// Wrapper to fetch extension prompts by module name
2893+ const hasFilter = typeof prompt.filter === 'function';
2890-export function getExtensionPromptByName(moduleName) {
2894+ if (hasFilter && !await prompt.filter()) {
2891- if (moduleName) {
2895+ continue;
2892- return substituteParams(extension_prompts[moduleName]?.value);
2893- } else {
2894- return;
28952896 }
2897+
2898+ values.push(value);
2899+ }
2900+
2901+ return substituteParams(values.join('\n'));
2902+}
2903+
2904+/**
2905+ * Wrapper to fetch extension prompts by module name
2906+ * @param {string} moduleName Module name
2907+ * @returns {Promise<string>} Extension prompt
2908+ */
2909+export async function getExtensionPromptByName(moduleName) {
2910+ if (!moduleName) {
2911+ return '';
2912+ }
2913+
2914+ const prompt = extension_prompts[moduleName];
2915+
2916+ if (!prompt) {
2917+ return '';
2918+ }
2919+
2920+ const hasFilter = typeof prompt.filter === 'function';
2921+
2922+ if (hasFilter && !await prompt.filter()) {
2923+ return '';
2924+ }
2925+
2926+ return substituteParams(prompt.value);
28962927}
28972928
28982929/**
@@ -2903,27 +2934,36 @@ export function getExtensionPromptByName(moduleName) {
29032934 * @param {string} [separator] Separator for joining multiple prompts
29042935 * @param {number} [role] Role of the prompt
29052936 * @param {boolean} [wrap] Wrap start and end with a separator
29062937 * @returns {Promise<string>} Extension prompt
29072938 */
29082939export async function getExtensionPrompt(position = extension_prompt_types.IN_PROMPT, depth = undefined, separator = '\n', role = undefined, wrap = true) {
2909- let extension_prompt = Object.keys(extension_prompts)
2940+ const filterByFunction = async (prompt) => {
2941+ const hasFilter = typeof prompt.filter === 'function';
2942+ if (hasFilter && !await prompt.filter()) {
2943+ return false;
2944+ }
2945+ return true;
2946+ };
2947+ const promptPromises = Object.keys(extension_prompts)
29102948 .sort()
29112949 .map((x) => extension_prompts[x])
29122950 .filter(x => x.position == position && x.value)
29132951 .filter(x => depth === undefined || x.depth === undefined || x.depth === depth)
29142952 .filter(x => role === undefined || x.role === undefined || x.role === role)
2915- .map(x => x.value.trim())
2953+ .filter(filterByFunction);
2916- .join(separator);
2954+ const prompts = await Promise.all(promptPromises);
2917- if (wrap && extension_prompt.length && !extension_prompt.startsWith(separator)) {
2955+
2918- extension_prompt = separator + extension_prompt;
2956+ let values = prompts.map(x => x.value.trim()).join(separator);
2957+ if (wrap && values.length && !values.startsWith(separator)) {
2958+ values = separator + values;
29192959 }
29202960 if (wrap && extension_promptvalues.length && !extension_promptvalues.endsWith(separator)) {
29212961 extension_promptvalues = extension_promptvalues + separator;
29222962 }
29232963 if (extension_promptvalues.length) {
29242964 extension_promptvalues = substituteParams(extension_promptvalues);
29252965 }
29262966 return extension_promptvalues;
29272967}
29282968
29292969export function baseChatReplace(value, name1, name2) {
@@ -3837,7 +3877,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
38373877 // Inject all Depth prompts. Chat Completion does it separately
38383878 let injectedIndices = [];
38393879 if (main_api !== 'openai') {
38403880 injectedIndices = await doChatInject(coreChat, isContinue);
38413881 }
38423882
38433883 // Insert character jailbreak as the last user message (if exists, allowed, preferred, and not using Chat Completion)
@@ -3910,8 +3950,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
39103950 }
39113951
39123952 // Call combined AN into Generate
39133953 const beforeScenarioAnchor = (await getExtensionPrompt(extension_prompt_types.BEFORE_PROMPT)).trimStart();
39143954 const afterScenarioAnchor = await getExtensionPrompt(extension_prompt_types.IN_PROMPT);
39153955
39163956 const storyStringParams = {
39173957 description: description,
@@ -4474,7 +4514,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
44744514 ...thisPromptBits[currentArrayEntry],
44754515 rawPrompt: generate_data.prompt || generate_data.input,
44764516 mesId: getNextMessageId(type),
44774517 allAnchors: await getAllExtensionPrompts(),
44784518 chatInjects: injectedIndices?.map(index => arrMes[arrMes.length - index - 1])?.join('') || '',
44794519 summarizeString: (extension_prompts['1_memory']?.value || ''),
44804520 authorsNoteString: (extension_prompts['2_floating_prompt']?.value || ''),
@@ -4743,9 +4783,9 @@ export function stopGeneration() {
47434783 * Injects extension prompts into chat messages.
47444784 * @param {object[]} messages Array of chat messages
47454785 * @param {boolean} isContinue Whether the generation is a continuation. If true, the extension prompts of depth 0 are injected at position 1.
47464786 * @returns {Promise<number[]>} Array of indices where the extension prompts were injected
47474787 */
47484788async function doChatInject(messages, isContinue) {
47494789 const injectedIndices = [];
47504790 let totalInsertedMessages = 0;
47514791 messages.reverse();
@@ -4763,7 +4803,7 @@ function doChatInject(messages, isContinue) {
47634803 const wrap = false;
47644804
47654805 for (const role of roles) {
47664806 const extensionPrompt = String(await getExtensionPrompt(extension_prompt_types.IN_CHAT, i, separator, role, wrap)).trimStart();
47674807 const isNarrator = role === extension_prompt_roles.SYSTEM;
47684808 const isUser = role === extension_prompt_roles.USER;
47694809 const name = names[role];
@@ -7456,14 +7496,16 @@ function select_rm_characters() {
74567496 * @param {number} depth Insertion depth. 0 represets the last message in context. Expected values up to MAX_INJECTION_DEPTH.
74577497 * @param {number} role Extension prompt role. Defaults to SYSTEM.
74587498 * @param {boolean} scan Should the prompt be included in the world info scan.
7499+ * @param {(function(): Promise<boolean>|boolean)} filter Filter function to determine if the prompt should be injected.
74597500 */
74607501export function setExtensionPrompt(key, value, position, depth, scan = false, role = extension_prompt_roles.SYSTEM, filter = null) {
74617502 extension_prompts[key] = {
74627503 value: String(value),
74637504 position: Number(position),
74647505 depth: Number(depth),
74657506 scan: !!scan,
74667507 role: Number(role ?? extension_prompt_roles.SYSTEM),
7508+ filter: filter,
74677509 };
74687510}
74697511
public/scripts/instruct-mode.js+5 -4
@@ -431,7 +431,8 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) {
431431 return mesExamplesArray.map(x => x.replace(/<START>\n/i, blockHeading));
432432 }
433433
434434 const includeNames = power_user.instruct.names_behavior === names_behavior_types.ALWAYS || (!!selected_group && power_user.instruct.names_behavior === names_behavior_types.FORCE);
435+ const includeGroupNames = selected_group && [names_behavior_types.ALWAYS, names_behavior_types.FORCE].includes(power_user.instruct.names_behavior);
435436
436437 let inputPrefix = power_user.instruct.input_sequence || '';
437438 let outputPrefix = power_user.instruct.output_sequence || '';
@@ -463,7 +464,7 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) {
463464
464465 for (const item of mesExamplesArray) {
465466 const cleanedItem = item.replace(/<START>/i, '{Example Dialogue:}').replace(/\r/gm, '');
466467 const blockExamples = parseExampleIntoIndividual(cleanedItem, includeGroupNames);
467468
468469 if (blockExamples.length === 0) {
469470 continue;
@@ -474,8 +475,9 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) {
474475 }
475476
476477 for (const example of blockExamples) {
478+ // If group names were already included, we don't want to add an additional prefix
477479 // If force group/persona names is set, we should override the include names for the user placeholder
478480 const includeThisName = (includeNames && !includeGroupNames) || (power_user.instruct.names_behavior === names_behavior_types.FORCE && example.name == 'example_user');
479481
480482 const prefix = example.name == 'example_user' ? inputPrefix : outputPrefix;
481483 const suffix = example.name == 'example_user' ? inputSuffix : outputSuffix;
@@ -489,7 +491,6 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) {
489491 if (formattedExamples.length === 0) {
490492 return mesExamplesArray.map(x => x.replace(/<START>\n/i, blockHeading));
491493 }
492-
493494 return formattedExamples;
494495}
495496
public/scripts/openai.js+10 -6
@@ -611,8 +611,9 @@ function formatWorldInfo(value) {
611611 *
612612 * @param {Prompt[]} prompts - Array containing injection prompts.
613613 * @param {Object[]} messages - Array containing all messages.
614+ * @returns {Promise<Object[]>} - Array containing all messages with injections.
614615 */
615616async function populationInjectionPrompts(prompts, messages) {
616617 let totalInsertedMessages = 0;
617618
618619 const roleTypes = {
@@ -635,7 +636,7 @@ function populationInjectionPrompts(prompts, messages) {
635636 // Get prompts for current role
636637 const rolePrompts = depthPrompts.filter(prompt => prompt.role === role).map(x => x.content).join(separator);
637638 // Get extension prompt
638639 const extensionPrompt = await getExtensionPrompt(extension_prompt_types.IN_CHAT, i, separator, roleTypes[role], wrap);
639640
640641 const jointPrompt = [rolePrompts, extensionPrompt].filter(x => x).map(x => x.trim()).join(separator);
641642
@@ -1020,7 +1021,7 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm
10201021 }
10211022
10221023 // Add in-chat injections
10231024 messages = await populationInjectionPrompts(absolutePrompts, messages);
10241025
10251026 // Decide whether dialogue examples should always be added
10261027 if (power_user.pin_examples) {
@@ -1051,9 +1052,9 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm
10511052 * @param {string} options.systemPromptOverride
10521053 * @param {string} options.jailbreakPromptOverride
10531054 * @param {string} options.personaDescription
10541055 * @returns {Promise<Object>} prompts - The prepared and merged system and user-defined prompts.
10551056 */
10561057async function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, worldInfoBefore, worldInfoAfter, charDescription, quietPrompt, bias, extensionPrompts, systemPromptOverride, jailbreakPromptOverride, personaDescription }) {
10571058 const scenarioText = Scenario && oai_settings.scenario_format ? substituteParams(oai_settings.scenario_format) : '';
10581059 const charPersonalityText = charPersonality && oai_settings.personality_format ? substituteParams(oai_settings.personality_format) : '';
10591060 const groupNudge = substituteParams(oai_settings.group_nudge_prompt);
@@ -1142,6 +1143,9 @@ function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, wor
11421143 if (!extensionPrompts[key].value) continue;
11431144 if (![extension_prompt_types.BEFORE_PROMPT, extension_prompt_types.IN_PROMPT].includes(prompt.position)) continue;
11441145
1146+ const hasFilter = typeof prompt.filter === 'function';
1147+ if (hasFilter && !await prompt.filter()) continue;
1148+
11451149 systemPrompts.push({
11461150 identifier: key.replace(/\W/g, '_'),
11471151 position: getPromptPosition(prompt.position),
@@ -1254,7 +1258,7 @@ export async function prepareOpenAIMessages({
12541258
12551259 try {
12561260 // Merge markers and ordered user prompts with system prompts
12571261 const prompts = await preparePromptsForChatCompletion({
12581262 Scenario,
12591263 charPersonality,
12601264 name2,
public/scripts/slash-commands.js+63 -7
@@ -84,6 +84,25 @@ export const parser = new SlashCommandParser();
8484const registerSlashCommand = SlashCommandParser.addCommand.bind(SlashCommandParser);
8585const getSlashCommandsHelp = parser.getHelpString.bind(parser);
8686
87+/**
88+ * Converts a SlashCommandClosure to a filter function that returns a boolean.
89+ * @param {SlashCommandClosure} closure
90+ * @returns {() => Promise<boolean>}
91+ */
92+function closureToFilter(closure) {
93+ return async () => {
94+ try {
95+ const localClosure = closure.getCopy();
96+ localClosure.onProgress = () => { };
97+ const result = await localClosure.execute();
98+ return isTrueBoolean(result.pipe);
99+ } catch (e) {
100+ console.error('Error executing filter closure', e);
101+ return false;
102+ }
103+ };
104+}
105+
87106export function initDefaultSlashCommands() {
88107 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
89108 name: '?',
@@ -1611,6 +1630,13 @@ export function initDefaultSlashCommands() {
16111630 new SlashCommandNamedArgument(
16121631 'ephemeral', 'remove injection after generation', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
16131632 ),
1633+ SlashCommandNamedArgument.fromProps({
1634+ name: 'filter',
1635+ description: 'if a filter is defined, an injection will only be performed if the closure returns true',
1636+ typeList: [ARGUMENT_TYPE.CLOSURE],
1637+ isRequired: false,
1638+ acceptsMultiple: false,
1639+ }),
16141640 ],
16151641 unnamedArgumentList: [
16161642 new SlashCommandArgument(
@@ -1901,6 +1927,11 @@ const NARRATOR_NAME_DEFAULT = 'System';
19011927export const COMMENT_NAME_DEFAULT = 'Note';
19021928const SCRIPT_PROMPT_KEY = 'script_inject_';
19031929
1930+/**
1931+ * Adds a new script injection to the chat.
1932+ * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments
1933+ * @param {import('./slash-commands/SlashCommand.js').UnnamedArguments} value Unnamed argument
1934+ */
19041935function injectCallback(args, value) {
19051936 const positions = {
19061937 'before': extension_prompt_types.BEFORE_PROMPT,
@@ -1914,8 +1945,8 @@ function injectCallback(args, value) {
19141945 'assistant': extension_prompt_roles.ASSISTANT,
19151946 };
19161947
19171948 const id = String(args?.id);
19181949 const ephemeral = isTrueBoolean(String(args?.ephemeral));
19191950
19201951 if (!id) {
19211952 console.warn('WARN: No ID provided for /inject command');
@@ -1931,9 +1962,15 @@ function injectCallback(args, value) {
19311962 const depth = isNaN(depthValue) ? defaultDepth : depthValue;
19321963 const roleValue = typeof args?.role === 'string' ? args.role.toLowerCase().trim() : Number(args?.role ?? extension_prompt_roles.SYSTEM);
19331964 const role = roles[roleValue] ?? roles[extension_prompt_roles.SYSTEM];
19341965 const scan = isTrueBoolean(String(args?.scan));
1966+ const filter = args?.filter instanceof SlashCommandClosure ? args.filter.rawText : null;
1967+ const filterFunction = args?.filter instanceof SlashCommandClosure ? closureToFilter(args.filter) : null;
19351968 value = value || '';
19361969
1970+ if (args?.filter && !String(filter ?? '').trim()) {
1971+ throw new Error('Failed to parse the filter argument. Make sure it is a valid non-empty closure.');
1972+ }
1973+
19371974 const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`;
19381975
19391976 if (!chat_metadata.script_injects) {
@@ -1941,13 +1978,13 @@ function injectCallback(args, value) {
19411978 }
19421979
19431980 if (value) {
19441981 const inject = { value, position, depth, scan, role, filter };
19451982 chat_metadata.script_injects[id] = inject;
19461983 } else {
19471984 delete chat_metadata.script_injects[id];
19481985 }
19491986
19501987 setExtensionPrompt(prefixedId, String(value), position, depth, scan, role, filterFunction);
19511988 saveMetadataDebounced();
19521989
19531990 if (ephemeral) {
@@ -1958,7 +1995,7 @@ function injectCallback(args, value) {
19581995 }
19591996 console.log('Removing ephemeral script injection', id);
19601997 delete chat_metadata.script_injects[id];
19611998 setExtensionPrompt(prefixedId, '', position, depth, scan, role, filterFunction);
19621999 saveMetadataDebounced();
19632000 deleted = true;
19642001 };
@@ -2053,9 +2090,28 @@ export function processChatSlashCommands() {
20532090 }
20542091
20552092 for (const [id, inject] of Object.entries(context.chatMetadata.script_injects)) {
2093+ /**
2094+ * Rehydrates a filter closure from a string.
2095+ * @returns {SlashCommandClosure | null}
2096+ */
2097+ function reviveFilterClosure() {
2098+ if (!inject.filter) {
2099+ return null;
2100+ }
2101+
2102+ try {
2103+ return new SlashCommandParser().parse(inject.filter, true);
2104+ } catch (error) {
2105+ console.warn('Failed to revive filter closure for script injection', id, error);
2106+ return null;
2107+ }
2108+ }
2109+
20562110 const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`;
2111+ const filterClosure = reviveFilterClosure();
2112+ const filter = filterClosure ? closureToFilter(filterClosure) : null;
20572113 console.log('Adding script injection', id);
20582114 setExtensionPrompt(prefixedId, inject.value, inject.position, inject.depth, inject.scan, inject.role, filter);
20592115 }
20602116}
20612117
public/scripts/st-context.js+3 -1
@@ -63,7 +63,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
6363import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
6464import { tag_map, tags } from './tags.js';
6565import { textgenerationwebui_settings } from './textgen-settings.js';
6666import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';
6767import { ToolManager } from './tool-calling.js';
6868import { timestampToMoment } from './utils.js';
6969
@@ -95,6 +95,8 @@ export function getContext() {
9595 sendStreamingRequest,
9696 sendGenerationRequest,
9797 stopGeneration,
98+ tokenizers,
99+ getTextTokens,
98100 /** @deprecated Use getTokenCountAsync instead */
99101 getTokenCount,
100102 getTokenCountAsync,
public/scripts/world-info.js+1 -1
@@ -3760,7 +3760,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
37603760 // Put this code here since otherwise, the chat reference is modified
37613761 for (const key of Object.keys(context.extensionPrompts)) {
37623762 if (context.extensionPrompts[key]?.scan) {
37633763 const prompt = await getExtensionPromptByName(key);
37643764 if (prompt) {
37653765 buffer.addInject(prompt);
37663766 }