From d6f34f7b2c812462030be6af198b4b982808d895 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 6 Dec 2024 19:53:02 +0200 Subject: [PATCH 1/7] Add prompt injection filters --- public/script.js | 108 ++++++++++++++++++++++++----------- public/scripts/openai.js | 16 ++++-- public/scripts/world-info.js | 2 +- 3 files changed, 86 insertions(+), 40 deletions(-) diff --git a/public/script.js b/public/script.js index 5ebc7592e..5648d90b6 100644 --- a/public/script.js +++ b/public/script.js @@ -2875,23 +2875,54 @@ function addPersonaDescriptionExtensionPrompt() { } } -function getAllExtensionPrompts() { - const value = Object - .values(extension_prompts) - .filter(x => x.value) - .map(x => x.value.trim()) - .join('\n'); +/** + * Returns all extension prompts combined. + * @returns {Promise} Combined extension prompts + */ +async function getAllExtensionPrompts() { + const values = []; - return value.length ? substituteParams(value) : ''; + for (const prompt of Object.values(extension_prompts)) { + const value = prompt?.value?.trim(); + + if (!value) { + continue; + } + + const hasFilter = typeof prompt.filter === 'function'; + if (hasFilter && !await prompt.filter()) { + continue; + } + + values.push(value); + } + + return substituteParams(values.join('\n')); } -// Wrapper to fetch extension prompts by module name -export function getExtensionPromptByName(moduleName) { - if (moduleName) { - return substituteParams(extension_prompts[moduleName]?.value); - } else { - return; +/** + * Wrapper to fetch extension prompts by module name + * @param {string} moduleName Module name + * @returns {Promise} Extension prompt + */ +export async function getExtensionPromptByName(moduleName) { + if (!moduleName) { + return ''; } + + const prompt = extension_prompts[moduleName]; + + if (!prompt) { + return ''; + } + + const hasFilter = typeof prompt.filter === 'function'; + + if (hasFilter && !await prompt.filter()) { + return ''; + } + + return substituteParams(prompt.value); } /** @@ -2902,27 +2933,36 @@ export function getExtensionPromptByName(moduleName) { * @param {string} [separator] Separator for joining multiple prompts * @param {number} [role] Role of the prompt * @param {boolean} [wrap] Wrap start and end with a separator - * @returns {string} Extension prompt + * @returns {Promise} Extension prompt */ -export function getExtensionPrompt(position = extension_prompt_types.IN_PROMPT, depth = undefined, separator = '\n', role = undefined, wrap = true) { - let extension_prompt = Object.keys(extension_prompts) +export async function getExtensionPrompt(position = extension_prompt_types.IN_PROMPT, depth = undefined, separator = '\n', role = undefined, wrap = true) { + const filterByFunction = async (prompt) => { + const hasFilter = typeof prompt.filter === 'function'; + if (hasFilter && !await prompt.filter()) { + return false; + } + return true; + }; + const promptPromises = Object.keys(extension_prompts) .sort() .map((x) => extension_prompts[x]) .filter(x => x.position == position && x.value) .filter(x => depth === undefined || x.depth === undefined || x.depth === depth) .filter(x => role === undefined || x.role === undefined || x.role === role) - .map(x => x.value.trim()) - .join(separator); - if (wrap && extension_prompt.length && !extension_prompt.startsWith(separator)) { - extension_prompt = separator + extension_prompt; + .filter(filterByFunction); + const prompts = await Promise.all(promptPromises); + + let values = prompts.map(x => x.value.trim()).join(separator); + if (wrap && values.length && !values.startsWith(separator)) { + values = separator + values; } - if (wrap && extension_prompt.length && !extension_prompt.endsWith(separator)) { - extension_prompt = extension_prompt + separator; + if (wrap && values.length && !values.endsWith(separator)) { + values = values + separator; } - if (extension_prompt.length) { - extension_prompt = substituteParams(extension_prompt); + if (values.length) { + values = substituteParams(values); } - return extension_prompt; + return values; } export function baseChatReplace(value, name1, name2) { @@ -3836,7 +3876,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro // Inject all Depth prompts. Chat Completion does it separately let injectedIndices = []; if (main_api !== 'openai') { - injectedIndices = doChatInject(coreChat, isContinue); + injectedIndices = await doChatInject(coreChat, isContinue); } // Insert character jailbreak as the last user message (if exists, allowed, preferred, and not using Chat Completion) @@ -3909,8 +3949,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro } // Call combined AN into Generate - const beforeScenarioAnchor = getExtensionPrompt(extension_prompt_types.BEFORE_PROMPT).trimStart(); - const afterScenarioAnchor = getExtensionPrompt(extension_prompt_types.IN_PROMPT); + const beforeScenarioAnchor = (await getExtensionPrompt(extension_prompt_types.BEFORE_PROMPT)).trimStart(); + const afterScenarioAnchor = await getExtensionPrompt(extension_prompt_types.IN_PROMPT); const storyStringParams = { description: description, @@ -4473,7 +4513,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro ...thisPromptBits[currentArrayEntry], rawPrompt: generate_data.prompt || generate_data.input, mesId: getNextMessageId(type), - allAnchors: getAllExtensionPrompts(), + allAnchors: await getAllExtensionPrompts(), chatInjects: injectedIndices?.map(index => arrMes[arrMes.length - index - 1])?.join('') || '', summarizeString: (extension_prompts['1_memory']?.value || ''), authorsNoteString: (extension_prompts['2_floating_prompt']?.value || ''), @@ -4742,9 +4782,9 @@ export function stopGeneration() { * Injects extension prompts into chat messages. * @param {object[]} messages Array of chat messages * @param {boolean} isContinue Whether the generation is a continuation. If true, the extension prompts of depth 0 are injected at position 1. - * @returns {number[]} Array of indices where the extension prompts were injected + * @returns {Promise} Array of indices where the extension prompts were injected */ -function doChatInject(messages, isContinue) { +async function doChatInject(messages, isContinue) { const injectedIndices = []; let totalInsertedMessages = 0; messages.reverse(); @@ -4762,7 +4802,7 @@ function doChatInject(messages, isContinue) { const wrap = false; for (const role of roles) { - const extensionPrompt = String(getExtensionPrompt(extension_prompt_types.IN_CHAT, i, separator, role, wrap)).trimStart(); + const extensionPrompt = String(await getExtensionPrompt(extension_prompt_types.IN_CHAT, i, separator, role, wrap)).trimStart(); const isNarrator = role === extension_prompt_roles.SYSTEM; const isUser = role === extension_prompt_roles.USER; const name = names[role]; @@ -7455,14 +7495,16 @@ function select_rm_characters() { * @param {number} depth Insertion depth. 0 represets the last message in context. Expected values up to MAX_INJECTION_DEPTH. * @param {number} role Extension prompt role. Defaults to SYSTEM. * @param {boolean} scan Should the prompt be included in the world info scan. + * @param {(function(): Promise|boolean)} filter Filter function to determine if the prompt should be injected. */ -export function setExtensionPrompt(key, value, position, depth, scan = false, role = extension_prompt_roles.SYSTEM) { +export function setExtensionPrompt(key, value, position, depth, scan = false, role = extension_prompt_roles.SYSTEM, filter = null) { extension_prompts[key] = { value: String(value), position: Number(position), depth: Number(depth), scan: !!scan, role: Number(role ?? extension_prompt_roles.SYSTEM), + filter: filter, }; } diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 1f5b4d8f9..1162db674 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -611,8 +611,9 @@ function formatWorldInfo(value) { * * @param {Prompt[]} prompts - Array containing injection prompts. * @param {Object[]} messages - Array containing all messages. + * @returns {Promise} - Array containing all messages with injections. */ -function populationInjectionPrompts(prompts, messages) { +async function populationInjectionPrompts(prompts, messages) { let totalInsertedMessages = 0; const roleTypes = { @@ -635,7 +636,7 @@ function populationInjectionPrompts(prompts, messages) { // Get prompts for current role const rolePrompts = depthPrompts.filter(prompt => prompt.role === role).map(x => x.content).join(separator); // Get extension prompt - const extensionPrompt = getExtensionPrompt(extension_prompt_types.IN_CHAT, i, separator, roleTypes[role], wrap); + const extensionPrompt = await getExtensionPrompt(extension_prompt_types.IN_CHAT, i, separator, roleTypes[role], wrap); const jointPrompt = [rolePrompts, extensionPrompt].filter(x => x).map(x => x.trim()).join(separator); @@ -1020,7 +1021,7 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm } // Add in-chat injections - messages = populationInjectionPrompts(absolutePrompts, messages); + messages = await populationInjectionPrompts(absolutePrompts, messages); // Decide whether dialogue examples should always be added if (power_user.pin_examples) { @@ -1051,9 +1052,9 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm * @param {string} options.systemPromptOverride * @param {string} options.jailbreakPromptOverride * @param {string} options.personaDescription - * @returns {Object} prompts - The prepared and merged system and user-defined prompts. + * @returns {Promise} prompts - The prepared and merged system and user-defined prompts. */ -function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, worldInfoBefore, worldInfoAfter, charDescription, quietPrompt, bias, extensionPrompts, systemPromptOverride, jailbreakPromptOverride, personaDescription }) { +async function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, worldInfoBefore, worldInfoAfter, charDescription, quietPrompt, bias, extensionPrompts, systemPromptOverride, jailbreakPromptOverride, personaDescription }) { const scenarioText = Scenario && oai_settings.scenario_format ? substituteParams(oai_settings.scenario_format) : ''; const charPersonalityText = charPersonality && oai_settings.personality_format ? substituteParams(oai_settings.personality_format) : ''; const groupNudge = substituteParams(oai_settings.group_nudge_prompt); @@ -1142,6 +1143,9 @@ function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, wor if (!extensionPrompts[key].value) continue; if (![extension_prompt_types.BEFORE_PROMPT, extension_prompt_types.IN_PROMPT].includes(prompt.position)) continue; + const hasFilter = typeof prompt.filter === 'function'; + if (hasFilter && !await prompt.filter()) continue; + systemPrompts.push({ identifier: key.replace(/\W/g, '_'), position: getPromptPosition(prompt.position), @@ -1252,7 +1256,7 @@ export async function prepareOpenAIMessages({ try { // Merge markers and ordered user prompts with system prompts - const prompts = preparePromptsForChatCompletion({ + const prompts = await preparePromptsForChatCompletion({ Scenario, charPersonality, name2, diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 0f4351933..c92845d4a 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -3721,7 +3721,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) { // Put this code here since otherwise, the chat reference is modified for (const key of Object.keys(context.extensionPrompts)) { if (context.extensionPrompts[key]?.scan) { - const prompt = getExtensionPromptByName(key); + const prompt = await getExtensionPromptByName(key); if (prompt) { buffer.addInject(prompt); } From 205f1d7adb0d86b4510e0be8fe6163b70296ebfa Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 13 Dec 2024 00:02:24 +0200 Subject: [PATCH 2/7] Add filter arg for inject command --- public/scripts/slash-commands.js | 61 ++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 23a36828f..7fa9e1582 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -84,6 +84,20 @@ export const parser = new SlashCommandParser(); const registerSlashCommand = SlashCommandParser.addCommand.bind(SlashCommandParser); const getSlashCommandsHelp = parser.getHelpString.bind(parser); +/** + * Converts a SlashCommandClosure to a filter function that returns a boolean. + * @param {SlashCommandClosure} closure + * @returns {() => Promise} + */ +function closureToFilter(closure) { + return async () => { + const localClosure = closure.getCopy(); + localClosure.onProgress = () => { }; + const result = await localClosure.execute(); + return isTrueBoolean(result.pipe); + }; +} + export function initDefaultSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: '?', @@ -1611,6 +1625,13 @@ export function initDefaultSlashCommands() { new SlashCommandNamedArgument( 'ephemeral', 'remove injection after generation', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ), + SlashCommandNamedArgument.fromProps({ + name: 'filter', + description: 'if a filter is defined, an injection will only be performed if the closure returns true', + typeList: [ARGUMENT_TYPE.CLOSURE], + isRequired: false, + acceptsMultiple: false, + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -1901,6 +1922,11 @@ const NARRATOR_NAME_DEFAULT = 'System'; export const COMMENT_NAME_DEFAULT = 'Note'; const SCRIPT_PROMPT_KEY = 'script_inject_'; +/** + * Adds a new script injection to the chat. + * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments + * @param {import('./slash-commands/SlashCommand.js').UnnamedArguments} value Unnamed argument + */ function injectCallback(args, value) { const positions = { 'before': extension_prompt_types.BEFORE_PROMPT, @@ -1914,8 +1940,8 @@ function injectCallback(args, value) { 'assistant': extension_prompt_roles.ASSISTANT, }; - const id = args?.id; - const ephemeral = isTrueBoolean(args?.ephemeral); + const id = String(args?.id); + const ephemeral = isTrueBoolean(String(args?.ephemeral)); if (!id) { console.warn('WARN: No ID provided for /inject command'); @@ -1931,7 +1957,9 @@ function injectCallback(args, value) { const depth = isNaN(depthValue) ? defaultDepth : depthValue; const roleValue = typeof args?.role === 'string' ? args.role.toLowerCase().trim() : Number(args?.role ?? extension_prompt_roles.SYSTEM); const role = roles[roleValue] ?? roles[extension_prompt_roles.SYSTEM]; - const scan = isTrueBoolean(args?.scan); + const scan = isTrueBoolean(String(args?.scan)); + const filter = args?.filter instanceof SlashCommandClosure ? args.filter.rawText : null; + const filterFunction = args?.filter instanceof SlashCommandClosure ? closureToFilter(args.filter) : null; value = value || ''; const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`; @@ -1941,13 +1969,13 @@ function injectCallback(args, value) { } if (value) { - const inject = { value, position, depth, scan, role }; + const inject = { value, position, depth, scan, role, filter }; chat_metadata.script_injects[id] = inject; } else { delete chat_metadata.script_injects[id]; } - setExtensionPrompt(prefixedId, value, position, depth, scan, role); + setExtensionPrompt(prefixedId, String(value), position, depth, scan, role, filterFunction); saveMetadataDebounced(); if (ephemeral) { @@ -1958,7 +1986,7 @@ function injectCallback(args, value) { } console.log('Removing ephemeral script injection', id); delete chat_metadata.script_injects[id]; - setExtensionPrompt(prefixedId, '', position, depth, scan, role); + setExtensionPrompt(prefixedId, '', position, depth, scan, role, filterFunction); saveMetadataDebounced(); deleted = true; }; @@ -2053,9 +2081,28 @@ export function processChatSlashCommands() { } for (const [id, inject] of Object.entries(context.chatMetadata.script_injects)) { + /** + * Rehydrates a filter closure from a string. + * @returns {SlashCommandClosure | null} + */ + function reviveFilterClosure() { + if (!inject.filter) { + return null; + } + + try { + return new SlashCommandParser().parse(inject.filter, true); + } catch (error) { + console.warn('Failed to revive filter closure for script injection', id, error); + return null; + } + } + const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`; + const filterClosure = reviveFilterClosure(); + const filter = filterClosure ? closureToFilter(filterClosure) : null; console.log('Adding script injection', id); - setExtensionPrompt(prefixedId, inject.value, inject.position, inject.depth, inject.scan, inject.role); + setExtensionPrompt(prefixedId, inject.value, inject.position, inject.depth, inject.scan, inject.role, filter); } } From 294b15976cf0b855e8b441457ca1e6dea3638630 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 13 Dec 2024 01:20:43 +0200 Subject: [PATCH 3/7] Add validation for filter inject argument --- public/scripts/slash-commands.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 7fa9e1582..004c522d3 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -1962,6 +1962,10 @@ function injectCallback(args, value) { const filterFunction = args?.filter instanceof SlashCommandClosure ? closureToFilter(args.filter) : null; value = value || ''; + if (args?.filter && !String(filter ?? '').trim()) { + throw new Error('Failed to parse the filter argument. Make sure it is a valid non-empty closure.'); + } + const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`; if (!chat_metadata.script_injects) { From 6f4350b3a7f074e57e1da5c150ec71415e43c712 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 13 Dec 2024 01:23:13 +0200 Subject: [PATCH 4/7] Add error handler to filter closure executor --- public/scripts/slash-commands.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 004c522d3..1ad0363fc 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -91,10 +91,15 @@ const getSlashCommandsHelp = parser.getHelpString.bind(parser); */ function closureToFilter(closure) { return async () => { - const localClosure = closure.getCopy(); - localClosure.onProgress = () => { }; - const result = await localClosure.execute(); - return isTrueBoolean(result.pipe); + try { + const localClosure = closure.getCopy(); + localClosure.onProgress = () => { }; + const result = await localClosure.execute(); + return isTrueBoolean(result.pipe); + } catch (e) { + console.error('Error executing filter closure', e); + return false; + } }; } From 0e5100180b38c47049cb51ec690e9de87f2ece0d Mon Sep 17 00:00:00 2001 From: Succubyss <87207237+Succubyss@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:29:34 -0600 Subject: [PATCH 5/7] expose tokenizers & getTextTokens in getContext --- public/scripts/st-context.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/scripts/st-context.js b/public/scripts/st-context.js index 4f0abdd8d..9cbf18210 100644 --- a/public/scripts/st-context.js +++ b/public/scripts/st-context.js @@ -63,7 +63,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from ' import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { tag_map, tags } from './tags.js'; import { textgenerationwebui_settings } from './textgen-settings.js'; -import { getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js'; +import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js'; import { ToolManager } from './tool-calling.js'; import { timestampToMoment } from './utils.js'; @@ -95,6 +95,8 @@ export function getContext() { sendStreamingRequest, sendGenerationRequest, stopGeneration, + tokenizers, + getTextTokens, /** @deprecated Use getTokenCountAsync instead */ getTokenCount, getTokenCountAsync, From 1ef25d617653a76197244d43f3cba465174c3cd5 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:56:06 -0700 Subject: [PATCH 6/7] fix double-prefixing on example messages --- public/scripts/instruct-mode.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/public/scripts/instruct-mode.js b/public/scripts/instruct-mode.js index 8d8556041..6875bb228 100644 --- a/public/scripts/instruct-mode.js +++ b/public/scripts/instruct-mode.js @@ -431,7 +431,8 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) { return mesExamplesArray.map(x => x.replace(/\n/i, blockHeading)); } - const includeNames = power_user.instruct.names_behavior === names_behavior_types.ALWAYS || (!!selected_group && power_user.instruct.names_behavior === names_behavior_types.FORCE); + const includeNames = power_user.instruct.names_behavior === names_behavior_types.ALWAYS; + const includeGroupNames = selected_group && (includeNames || power_user.instruct.names_behavior === names_behavior_types.FORCE); let inputPrefix = power_user.instruct.input_sequence || ''; let outputPrefix = power_user.instruct.output_sequence || ''; @@ -463,7 +464,7 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) { for (const item of mesExamplesArray) { const cleanedItem = item.replace(//i, '{Example Dialogue:}').replace(/\r/gm, ''); - const blockExamples = parseExampleIntoIndividual(cleanedItem); + const blockExamples = parseExampleIntoIndividual(cleanedItem, includeGroupNames); if (blockExamples.length === 0) { continue; @@ -474,8 +475,9 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) { } for (const example of blockExamples) { + // If group names were already included, we don't want to add an additional prefix // If force group/persona names is set, we should override the include names for the user placeholder - const includeThisName = includeNames || (power_user.instruct.names_behavior === names_behavior_types.FORCE && example.name == 'example_user'); + const includeThisName = (includeNames && !includeGroupNames) || (power_user.instruct.names_behavior === names_behavior_types.FORCE && example.name == 'example_user'); const prefix = example.name == 'example_user' ? inputPrefix : outputPrefix; const suffix = example.name == 'example_user' ? inputSuffix : outputSuffix; @@ -489,7 +491,6 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) { if (formattedExamples.length === 0) { return mesExamplesArray.map(x => x.replace(/\n/i, blockHeading)); } - return formattedExamples; } From a7e8d00145dfecd63fb055c7990c7bea443e32f5 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 14 Dec 2024 15:16:43 +0200 Subject: [PATCH 7/7] Use Array.includes --- public/scripts/instruct-mode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/instruct-mode.js b/public/scripts/instruct-mode.js index 6875bb228..73660ad65 100644 --- a/public/scripts/instruct-mode.js +++ b/public/scripts/instruct-mode.js @@ -432,7 +432,7 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) { } const includeNames = power_user.instruct.names_behavior === names_behavior_types.ALWAYS; - const includeGroupNames = selected_group && (includeNames || power_user.instruct.names_behavior === names_behavior_types.FORCE); + const includeGroupNames = selected_group && [names_behavior_types.ALWAYS, names_behavior_types.FORCE].includes(power_user.instruct.names_behavior); let inputPrefix = power_user.instruct.input_sequence || ''; let outputPrefix = power_user.instruct.output_sequence || '';