Compare commits

...

4 Commits

Author SHA1 Message Date
Cohee
5b270c3333 Merge branch 'staging' into CollapseSameEntityInstructMessages 2025-06-01 20:16:06 +03:00
RossAscends
ffe0e76812 console warn --> debug 2025-05-24 12:08:03 +09:00
RossAscends
7ccf05d160 fix doubled "label for=" 2025-05-24 12:02:13 +09:00
RossAscends
b5da8742e8 initial commit 2025-05-24 11:44:14 +09:00
3 changed files with 167 additions and 4 deletions

View File

@@ -140,7 +140,7 @@
<div data-preset-manager-import="novel" class="margin0 menu_button_icon menu_button" title="Import preset" data-i18n="[title]Import preset">
<i class="fa-fw fa-solid fa-file-import"></i>
</div>
<div data-preset-manager-export="novel" class="margin0 menu_button_icon menu_button" title="Export preset" data-i18n="[title]Export preset">
<div data-preset-manager-export="novel" class="margin0 menu_button_icon menu_button" title="Export preset" data-i18n="[title]Export preset">
<i class="fa-fw fa-solid fa-file-export"></i>
</div>
<div data-preset-manager-delete="novel" class="margin0 menu_button_icon menu_button" title="Delete the preset" data-i18n="[title]Delete the preset">
@@ -663,7 +663,7 @@
<div data-source="openrouter" class="range-block">
<div class="range-block-title" title="Allow compressing requests by removing messages from the middle of the prompt.">
<span data-i18n="Middle-out Transform">Middle-out Transform</span>
<a href="https://openrouter.ai/docs/transforms" target="_blank" rel="noreferrer noopener" class="note-link-span fa-solid fa-circle-question"></a>
<a href="https://openrouter.ai/docs/transforms" target="_blank" rel="noreferrer noopener" class="note-link-span fa-solid fa-circle-question"></a>
</div>
<div class="wide100p">
<select id="openrouter_middleout" class="text_pole">
@@ -3825,7 +3825,7 @@
<input id="instruct_bind_to_context" type="checkbox" style="display:none;" />
<small><i class="fa-solid fa-link menu_button margin0"></i></small>
</label>
<label id="instruct_enabled_label"for="instruct_enabled" class="checkbox_label flex1" title="Enable Instruct Mode" data-i18n="[title]instruct_enabled">
<label id="instruct_enabled_label" for="instruct_enabled" class="checkbox_label flex1" title="Enable Instruct Mode" data-i18n="[title]instruct_enabled">
<input id="instruct_enabled" type="checkbox" style="display:none;" />
<small><i class="fa-solid fa-power-off menu_button togglable margin0"></i></small>
</label>
@@ -3869,6 +3869,10 @@
<input id="instruct_skip_examples" type="checkbox" />
<small data-i18n="Skip Example Dialogues Formatting">Skip Example Dialogues Formatting</small>
</label>
<label for="instruct_collapse_same_entity" class="checkbox_label">
<input id="instruct_collapse_same_entity" type="checkbox" />
<small data-i18n="Collapse Consecutive Same-Entity Sequences" title="If enabled, all consecutive messages with the same entity will be merged in prompt." data-i18n="[title]If enabled, all consecutive messages with the same entity will be merged in prompt.">Collapse Consecutive Same-Entity Sequences</small>
</label>
<div>
<small data-i18n="Include Names">
Include Names
@@ -5284,7 +5288,7 @@
<div class="flex-container wide100p alignitemscenter spaceBetween flexNoGap">
<div class="flex-container alignItemsBaseline wide100p">
<div class="flex1 flex-container alignItemsBaseline">
<h3 class="margin0" >
<h3 class="margin0">
<span data-i18n="Persona Management">Persona Management</span>
<a href="https://docs.sillytavern.app/usage/core-concepts/personas/" target="_blank">
<span class="fa-solid fa-circle-question note-link-span"></span>

View File

@@ -3746,6 +3746,7 @@ export async function generateRaw(prompt, api, instructOverride, quietToLoud, sy
prompt = api == 'novel' ? adjustNovelInstructionPrompt(prompt) : prompt;
prompt = isInstruct ? formatInstructModeChat(name1, prompt, false, true, '', name1, name2, false) : prompt;
prompt = isInstruct ? (prompt + formatInstructModePrompt(name2, false, '', name1, name2, isQuiet, quietToLoud)) : (prompt + '\n');
prompt = isInstruct && power_user.instruct.collapse_same_entity ? (collapseConsecutiveMessagesInPrompt(prompt, power_user.instruct)) : prompt;
try {
if (responseLengthCustomized) {
@@ -3945,6 +3946,159 @@ function removeLastMessage() {
});
}
function collapseConsecutiveMessagesInPrompt(prompt, instruct) {
if (!instruct.enabled) {
console.debug('Instruct mode is disabled; returning original prompt');
return prompt;
}
if (!instruct.collapse_same_entity) {
console.debug('collapse_same_entity is false; returning original prompt');
return prompt;
}
console.debug('Collapsing consecutive messages in prompt');
// Define prefixes and suffixes
const prefixes = {
system: instruct.system_sequence,
system_first: instruct.system_sequence_prefix,
user: instruct.input_sequence,
assistant: instruct.output_sequence,
};
const suffixes = {
system_first: instruct.system_sequence_suffix,
system: instruct.system_suffix,
user: instruct.input_suffix,
assistant: instruct.output_suffix,
};
// Validate required settings
// Fail only if all prefixes and suffixes are empty strings
const allPrefixesEmpty = Object.values(prefixes).every(val => !val);
const allSuffixesEmpty = Object.values(suffixes).every(val => !val);
if (allPrefixesEmpty && allSuffixesEmpty) {
console.debug('All prefixes and suffixes are empty; returning original prompt', {
system_sequence: prefixes.system,
system_sequence_prefix: prefixes.system_first,
input_sequence: prefixes.user,
output_sequence: prefixes.assistant,
system_first_suffix: suffixes.system_first,
system_suffix: suffixes.system,
input_suffix: suffixes.user,
output_suffix: suffixes.assistant,
});
return prompt;
}
// Escape prefixes and suffixes for regex, handling empty strings
const escapeRegex = (str) => str ? str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : '';
const prefixRegexStr = Object.values(prefixes)
.filter(val => val)
.map(escapeRegex)
.join('|') || '\\b';
const suffixRegexStr = Object.values(suffixes)
.filter(val => val)
.map(escapeRegex)
.join('|') || '\\b';
// Use lookahead to match content up to suffix or next prefix
const messageRegex = new RegExp(
`(${prefixRegexStr})([\\s\\S]*?)(?=(?:${suffixRegexStr}|${prefixRegexStr}|$))((?:${suffixRegexStr})?)`,
'g',
);
// Parse the prompt into messages
const messages = [];
let match;
let lastIndex = 0;
console.debug('Parsing prompt with regex:', messageRegex);
while ((match = messageRegex.exec(prompt)) !== null) {
const [, prefix, content, suffix = ''] = match;
const prefixStart = match.index;
const gap = prompt.slice(lastIndex, prefixStart);
if (gap) {
console.debug(`Found gap between messages: ${JSON.stringify(gap)}`);
}
messages.push({ prefix, content: content.trim(), suffix, fullMatch: match[0], gap });
lastIndex = match.index + match[0].length;
console.debug(`Matched message: prefix=${prefix}, content=${content}, suffix=${suffix}`);
}
// Capture any trailing text
if (lastIndex < prompt.length) {
const trailing = prompt.slice(lastIndex);
console.debug(`Found trailing text: ${JSON.stringify(trailing)}`);
// Check if trailing text starts with a prefix
const trailingMatch = trailing.match(new RegExp(`^(${prefixRegexStr})([\\s\\S]*?)(?=(?:${suffixRegexStr}|${prefixRegexStr}|$))((?:${suffixRegexStr})?)`));
if (trailingMatch) {
const [, prefix, content, suffix = ''] = trailingMatch;
messages.push({ prefix, content: content.trim(), suffix, fullMatch: trailingMatch[0], gap: '' });
console.debug(`Matched trailing message: prefix=${prefix}, content=${content}, suffix=${suffix}`);
}
}
if (messages.length === 0) {
console.debug('No messages parsed; returning original prompt');
return prompt;
}
// Collapse consecutive messages with the same effective prefix
const collapsedMessages = [];
let i = 0;
while (i < messages.length) {
const current = messages[i];
const effectivePrefix = (i === 0 && current.prefix === prefixes.system_first) ? prefixes.system : current.prefix;
let combinedContent = [current.content];
let currentSuffix = current.suffix;
let j = i + 1;
// Avoid collapsing the last complete assistant message if followed by an incomplete assistant message
const isLastCompleteAssistant = i === messages.length - 2 &&
messages[i].prefix === prefixes.assistant &&
messages[i + 1].prefix === prefixes.assistant;
if (!isLastCompleteAssistant) {
// Collect consecutive messages with the same effective prefix
while (j < messages.length) {
const next = messages[j];
const nextEffectivePrefix = (j === 0 && next.prefix === prefixes.system_first) ? prefixes.system : next.prefix;
if (nextEffectivePrefix !== effectivePrefix) {
console.debug(`Stopping collapse at index ${j}: effectivePrefix=${effectivePrefix}, nextEffectivePrefix=${nextEffectivePrefix}`);
break;
}
combinedContent.push(next.content);
currentSuffix = next.suffix; // Use the last suffix
j++;
}
}
// Join contents with newlines
const joinedContent = combinedContent.reduce((acc, curr, idx) => {
if (idx === 0) return curr;
return acc + '\n' + curr;
}, '');
// Add newline after prefix if wrap is true and prefix doesn't end with newline
const prefixNewline = instruct.wrap && !current.prefix.match(/\n$/) ? '\n' : '';
// Create the collapsed message
const combinedMessage = `${current.prefix}${prefixNewline}${joinedContent}${currentSuffix}`;
collapsedMessages.push(combinedMessage);
console.debug(`Collapsed message at index ${i}: ${combinedMessage}`);
i = j; // Skip the combined messages
}
// Reconstruct the prompt without extra newlines
const collapsedPrompt = collapsedMessages.join('');
console.debug('Before collapsing:', prompt);
console.debug('After collapsing:', collapsedPrompt);
return collapsedPrompt;
}
/**
* MARK:Generate()
* Runs a generation using the current chat context.
@@ -4988,6 +5142,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
showStopButton();
if (isInstruct && power_user.instruct.collapse_same_entity) {
generate_data.prompt = collapseConsecutiveMessagesInPrompt(generate_data.prompt, power_user.instruct);
}
//set array object for prompt token itemization of this message
let currentArrayEntry = Number(thisPromptBits.length - 1);
let additionalPromptStuff = {

View File

@@ -43,6 +43,7 @@ const controls = [
{ id: 'instruct_derived', property: 'derived', isCheckbox: true },
{ id: 'instruct_bind_to_context', property: 'bind_to_context', isCheckbox: true },
{ id: 'instruct_skip_examples', property: 'skip_examples', isCheckbox: true },
{ id: 'instruct_collapse_same_entity', property: 'collapse_same_entity', isCheckbox: true },
{ id: 'instruct_names_behavior', property: 'names_behavior', isCheckbox: false },
{ id: 'instruct_system_same_as_user', property: 'system_same_as_user', isCheckbox: true, trigger: true },
];