Merge pull request #3370 from SillyTavern/reasoning-regex Add regex processing for reasoning blocks

ee57675c121fa7bf4d7e9d6ea94a2f1d214d1917

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

Signed
5 files changed, +53 -20Ignore whitespace
public/script.js+42 -17
@@ -1993,9 +1993,10 @@ export async function sendTextareaMessage() {
1993 * @param {boolean} isUser If the message was sent by the user1993 * @param {boolean} isUser If the message was sent by the user
1994 * @param {number} messageId Message index in chat array1994 * @param {number} messageId Message index in chat array
1995 * @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides1995 * @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides
1996 * @param {boolean} [isReasoning] If the message is reasoning output
1996 * @returns {string} HTML string1997 * @returns {string} HTML string
1997 */1998 */
1998export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}) {1999export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}, isReasoning = false) {
1999 if (!mes) {2000 if (!mes) {
2000 return '';2001 return '';
2001 }2002 }
@@ -2029,6 +2030,9 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
2029 if (!isSystem) {2030 if (!isSystem) {
2030 function getRegexPlacement() {2031 function getRegexPlacement() {
2031 try {2032 try {
2033 if (isReasoning) {
2034 return regex_placement.REASONING;
2035 }
2032 if (isUser) {2036 if (isUser) {
2033 return regex_placement.USER_INPUT;2037 return regex_placement.USER_INPUT;
2034 } else if (chat[messageId]?.extra?.type === 'narrator') {2038 } else if (chat[messageId]?.extra?.type === 'narrator') {
@@ -2250,8 +2254,8 @@ function getMessageFromTemplate({
2250export function updateMessageBlock(messageId, message) {2254export function updateMessageBlock(messageId, message) {
2251 const messageElement = $(`#chat [mesid="${messageId}"]`);2255 const messageElement = $(`#chat [mesid="${messageId}"]`);
2252 const text = message?.extra?.display_text ?? message.mes;2256 const text = message?.extra?.display_text ?? message.mes;
2253 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId));2257 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));
2254 messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, -1));2258 messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, messageId, {}, true));
2255 addCopyToCodeBlocks(messageElement);2259 addCopyToCodeBlocks(messageElement);
2256 appendMediaToMessage(message, messageElement);2260 appendMediaToMessage(message, messageElement);
2257}2261}
@@ -2408,9 +2412,10 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2408 mes.is_user,2412 mes.is_user,
2409 chat.indexOf(mes),2413 chat.indexOf(mes),
2410 sanitizerOverrides,2414 sanitizerOverrides,
2415 false,
2411 );2416 );
2412 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1);2417 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1, {}, false);
2413 const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, -1);2418 const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, chat.indexOf(mes), {}, true);
2414 let bookmarkLink = mes?.extra?.bookmark_link ?? '';2419 let bookmarkLink = mes?.extra?.bookmark_link ?? '';
24152420
2416 let params = {2421 let params = {
@@ -3205,7 +3210,7 @@ class StreamingProcessor {
3205 if (this.reasoning) {3210 if (this.reasoning) {
3206 chat[messageId]['extra']['reasoning'] = this.reasoning;3211 chat[messageId]['extra']['reasoning'] = this.reasoning;
3207 if (this.messageReasoningDom instanceof HTMLElement) {3212 if (this.messageReasoningDom instanceof HTMLElement) {
3208 const formattedReasoning = messageFormatting(this.reasoning, '', false, false, -1);3213 const formattedReasoning = messageFormatting(this.reasoning, '', false, false, messageId, {}, true);
3209 this.messageReasoningDom.innerHTML = formattedReasoning;3214 this.messageReasoningDom.innerHTML = formattedReasoning;
3210 }3215 }
3211 }3216 }
@@ -3232,6 +3237,8 @@ class StreamingProcessor {
3232 chat[messageId].is_system,3237 chat[messageId].is_system,
3233 chat[messageId].is_user,3238 chat[messageId].is_user,
3234 messageId,3239 messageId,
3240 {},
3241 false,
3235 );3242 );
3236 if (this.messageTextDom instanceof HTMLElement) {3243 if (this.messageTextDom instanceof HTMLElement) {
3237 this.messageTextDom.innerHTML = formattedText;3244 this.messageTextDom.innerHTML = formattedText;
@@ -3383,7 +3390,7 @@ class StreamingProcessor {
3383 if (logprobs) {3390 if (logprobs) {
3384 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));3391 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
3385 }3392 }
3386 this.reasoning = state?.reasoning ?? '';3393 this.reasoning = getRegexedString(state?.reasoning ?? '', regex_placement.REASONING);
3387 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);3394 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
3388 await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text));3395 await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text));
3389 }3396 }
@@ -3850,14 +3857,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
3850 coreChat.pop();3857 coreChat.pop();
3851 }3858 }
38523859
3853 const reasoning = new PromptReasoning();
3854 for (let i = coreChat.length - 1; i >= 0; i--) {
3855 if (reasoning.isLimitReached()) {
3856 break;
3857 }
3858 coreChat[i] = { ...coreChat[i], mes: reasoning.addToMessage(coreChat[i].mes, coreChat[i].extra?.reasoning) };
3859 }
3860
3861 coreChat = await Promise.all(coreChat.map(async (chatItem, index) => {3860 coreChat = await Promise.all(coreChat.map(async (chatItem, index) => {
3862 let message = chatItem.mes;3861 let message = chatItem.mes;
3863 let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT;3862 let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT;
@@ -3877,6 +3876,25 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
3877 };3876 };
3878 }));3877 }));
38793878
3879 const reasoning = new PromptReasoning();
3880 for (let i = coreChat.length - 1; i >= 0; i--) {
3881 if (reasoning.isLimitReached()) {
3882 break;
3883 }
3884 const depth = coreChat.length - i - 1;
3885 coreChat[i] = {
3886 ...coreChat[i],
3887 mes: reasoning.addToMessage(
3888 coreChat[i].mes,
3889 getRegexedString(
3890 String(coreChat[i].extra?.reasoning ?? ''),
3891 regex_placement.REASONING,
3892 { isPrompt: true, depth: depth },
3893 ),
3894 ),
3895 };
3896 }
3897
3880 // Determine token limit3898 // Determine token limit
3881 let this_max_context = getMaxContextSize();3899 let this_max_context = getMaxContextSize();
38823900
@@ -4785,6 +4803,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4785 const swipes = extractMultiSwipes(data, type);4803 const swipes = extractMultiSwipes(data, type);
47864804
4787 messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);4805 messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);
4806 reasoning = getRegexedString(reasoning, regex_placement.REASONING);
47884807
4789 if (isContinue) {4808 if (isContinue) {
4790 getMessage = continue_mag + getMessage;4809 getMessage = continue_mag + getMessage;
@@ -7190,9 +7209,11 @@ function messageEditAuto(div) {
7190 mes.is_system,7209 mes.is_system,
7191 mes.is_user,7210 mes.is_user,
7192 this_edit_mes_id,7211 this_edit_mes_id,
7212 {},
7213 false,
7193 ));7214 ));
7194 mesBlock.find('.mes_bias').empty();7215 mesBlock.find('.mes_bias').empty();
7195 mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1));7216 mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1, {}, false));
7196 saveChatDebounced();7217 saveChatDebounced();
7197}7218}
71987219
@@ -7214,10 +7235,12 @@ async function messageEditDone(div) {
7214 mes.is_system,7235 mes.is_system,
7215 mes.is_user,7236 mes.is_user,
7216 this_edit_mes_id,7237 this_edit_mes_id,
7238 {},
7239 false,
7217 ),7240 ),
7218 );7241 );
7219 mesBlock.find('.mes_bias').empty();7242 mesBlock.find('.mes_bias').empty();
7220 mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1));7243 mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1, {}, false));
7221 appendMediaToMessage(mes, div.closest('.mes'));7244 appendMediaToMessage(mes, div.closest('.mes'));
7222 addCopyToCodeBlocks(div.closest('.mes'));7245 addCopyToCodeBlocks(div.closest('.mes'));
72237246
@@ -10860,6 +10883,8 @@ jQuery(async function () {
10860 chat[this_edit_mes_id].is_system,10883 chat[this_edit_mes_id].is_system,
10861 chat[this_edit_mes_id].is_user,10884 chat[this_edit_mes_id].is_user,
10862 this_edit_mes_id,10885 this_edit_mes_id,
10886 {},
10887 false,
10863 ));10888 ));
10864 appendMediaToMessage(chat[this_edit_mes_id], $(this).closest('.mes'));10889 appendMediaToMessage(chat[this_edit_mes_id], $(this).closest('.mes'));
10865 addCopyToCodeBlocks($(this).closest('.mes'));10890 addCopyToCodeBlocks($(this).closest('.mes'));
public/scripts/extensions/regex/editor.html+6 -0
@@ -94,6 +94,12 @@
94 <span data-i18n="World Info">World Info</span>94 <span data-i18n="World Info">World Info</span>
95 </label>95 </label>
96 </div>96 </div>
97 <div data-i18n="[title]ext_regex_reasoning_desc" title="Reasoning block contents. When 'Only Format Prompt' is checked, it will also affect the reasoning contents added to the prompt.">
98 <label class="checkbox flex-container">
99 <input type="checkbox" name="replace_position" value="6">
100 <span data-i18n="Reasoning">Reasoning</span>
101 </label>
102 </div>
97 <div class="flex-container wide100p marginTop5">103 <div class="flex-container wide100p marginTop5">
98 <div class="flex1 flex-container flexNoGap">104 <div class="flex1 flex-container flexNoGap">
99 <small data-i18n="[title]ext_regex_min_depth_desc" title="When applied to prompts or display, only affect messages that are at least N levels deep. 0 = last message, 1 = penultimate message, etc. Only counts WI entries @Depth and usable messages, i.e. not hidden or system.">105 <small data-i18n="[title]ext_regex_min_depth_desc" title="When applied to prompts or display, only affect messages that are at least N levels deep. 0 = last message, 1 = penultimate message, etc. Only counts WI entries @Depth and usable messages, i.e. not hidden or system.">
public/scripts/extensions/regex/engine.js+2 -1
@@ -20,6 +20,7 @@ const regex_placement = {
20 SLASH_COMMAND: 3,20 SLASH_COMMAND: 3,
21 // 4 - sendAs (legacy)21 // 4 - sendAs (legacy)
22 WORLD_INFO: 5,22 WORLD_INFO: 5,
23 REASONING: 6,
23};24};
2425
25export const substitute_find_regex = {26export const substitute_find_regex = {
@@ -94,7 +95,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
94 // Script applies to Generate and input is Generate95 // Script applies to Generate and input is Generate
95 (script.promptOnly && isPrompt) ||96 (script.promptOnly && isPrompt) ||
96 // Script applies to all cases when neither "only"s are true, but there's no need to do it when `isMarkdown`, the as source (chat history) should already be changed beforehand97 // Script applies to all cases when neither "only"s are true, but there's no need to do it when `isMarkdown`, the as source (chat history) should already be changed beforehand
97 (!script.markdownOnly && !script.promptOnly && !isMarkdown)98 (!script.markdownOnly && !script.promptOnly && !isMarkdown && !isPrompt)
98 ) {99 ) {
99 if (isEdit && !script.runOnEdit) {100 if (isEdit && !script.runOnEdit) {
100 console.debug(`getRegexedString: Skipping script ${script.scriptName} because it does not run on edit`);101 console.debug(`getRegexedString: Skipping script ${script.scriptName} because it does not run on edit`);
public/scripts/extensions/regex/index.js+1 -1
@@ -18,7 +18,7 @@ import { t } from '../../i18n.js';
18 * @property {string} replaceString - The replace string18 * @property {string} replaceString - The replace string
19 * @property {string[]} trimStrings - The trim strings19 * @property {string[]} trimStrings - The trim strings
20 * @property {string?} findRegex - The find regex20 * @property {string?} findRegex - The find regex
21 * @property {string?} substituteRegex - The substitute regex21 * @property {number?} substituteRegex - The substitute regex
22 */22 */
2323
24/**24/**
public/scripts/reasoning.js+2 -1
@@ -1,4 +1,5 @@
1import { chat, closeMessageEditor, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';1import { chat, closeMessageEditor, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
2import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
2import { t } from './i18n.js';3import { t } from './i18n.js';
3import { MacrosParser } from './macros.js';4import { MacrosParser } from './macros.js';
4import { Popup } from './popup.js';5import { Popup } from './popup.js';
@@ -224,7 +225,7 @@ function setReasoningEventHandlers(){
224 }225 }
225226
226 const textarea = messageBlock.find('.reasoning_edit_textarea');227 const textarea = messageBlock.find('.reasoning_edit_textarea');
227 const reasoning = String(textarea.val());228 const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true });
228 message.extra.reasoning = reasoning;229 message.extra.reasoning = reasoning;
229 await saveChatConditional();230 await saveChatConditional();
230 updateMessageBlock(messageId, message);231 updateMessageBlock(messageId, message);