Merge pull request #3386 from SillyTavern/reasoning-parse Reasoning blocks auto-parsing

dfc2eb32c89bfe02506e2f1404e13d0622ba337c

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

Signed
5 files changed, +172 -3Showing whitespace changes
public/css/toggle-dependent.css+5 -0
@@ -472,6 +472,11 @@ label[for="trim_spaces"]:has(input:checked) i.warning {
472 display: none;472 display: none;
473}473}
474474
475label[for="trim_spaces"]:not(:has(input:checked)) small {
476 color: var(--warning);
477 opacity: 1;
478}
479
475#claude_function_prefill_warning {480#claude_function_prefill_warning {
476 display: none;481 display: none;
477 color: red;482 color: red;
public/index.html+6 -0
@@ -3791,6 +3791,12 @@
3791 <span data-i18n="Reasoning">Reasoning</span>3791 <span data-i18n="Reasoning">Reasoning</span>
3792 </h4>3792 </h4>
3793 <div>3793 <div>
3794 <label class="checkbox_label" for="reasoning_auto_parse" title="Automatically parse reasoning blocks from main content between the reasoning prefix/suffix. Both fields must be defined and non-empty." data-i18n="[title]reasoning_auto_parse">
3795 <input id="reasoning_auto_parse" type="checkbox" />
3796 <small data-i18n="Auto-Parse Reasoning">
3797 Auto-Parse Reasoning
3798 </small>
3799 </label>
3794 <label class="checkbox_label" for="reasoning_add_to_prompts" title="Add existing reasoning blocks to prompts. To add a new reasoning block, use the message edit menu." data-i18n="[title]reasoning_add_to_prompts">3800 <label class="checkbox_label" for="reasoning_add_to_prompts" title="Add existing reasoning blocks to prompts. To add a new reasoning block, use the message edit menu." data-i18n="[title]reasoning_add_to_prompts">
3795 <input id="reasoning_add_to_prompts" type="checkbox" />3801 <input id="reasoning_add_to_prompts" type="checkbox" />
3796 <small data-i18n="Add Reasoning to Prompts">3802 <small data-i18n="Add Reasoning to Prompts">
public/script.js+17 -1
@@ -170,6 +170,7 @@ import {
170 toggleDrawer,170 toggleDrawer,
171 isElementInViewport,171 isElementInViewport,
172 copyText,172 copyText,
173 escapeHtml,
173} from './scripts/utils.js';174} from './scripts/utils.js';
174import { debounce_timeout } from './scripts/constants.js';175import { debounce_timeout } from './scripts/constants.js';
175176
@@ -2067,6 +2068,17 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
2067 mes = mes.replaceAll('<', '&lt;').replaceAll('>', '&gt;');2068 mes = mes.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
2068 }2069 }
20692070
2071 // Make sure reasoning strings are always shown, even if they include "<" or ">"
2072 [power_user.reasoning.prefix, power_user.reasoning.suffix].forEach((reasoningString) => {
2073 if (!reasoningString || !reasoningString.trim().length) {
2074 return;
2075 }
2076 // Only replace the first occurrence of the reasoning string
2077 if (mes.includes(reasoningString)) {
2078 mes = mes.replace(reasoningString, escapeHtml(reasoningString));
2079 }
2080 });
2081
2070 if (!isSystem) {2082 if (!isSystem) {
2071 // Save double quotes in tags as a special character to prevent them from being encoded2083 // Save double quotes in tags as a special character to prevent them from being encoded
2072 if (!power_user.encode_tags) {2084 if (!power_user.encode_tags) {
@@ -3209,7 +3221,7 @@ class StreamingProcessor {
3209 }3221 }
32103222
3211 if (this.reasoning) {3223 if (this.reasoning) {
3212 chat[messageId]['extra']['reasoning'] = this.reasoning;3224 chat[messageId]['extra']['reasoning'] = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
3213 if (this.messageReasoningDom instanceof HTMLElement) {3225 if (this.messageReasoningDom instanceof HTMLElement) {
3214 const formattedReasoning = messageFormatting(this.reasoning, '', false, false, messageId, {}, true);3226 const formattedReasoning = messageFormatting(this.reasoning, '', false, false, messageId, {}, true);
3215 this.messageReasoningDom.innerHTML = formattedReasoning;3227 this.messageReasoningDom.innerHTML = formattedReasoning;
@@ -4778,6 +4790,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4778 messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);4790 messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);
4779 reasoning = getRegexedString(reasoning, regex_placement.REASONING);4791 reasoning = getRegexedString(reasoning, regex_placement.REASONING);
47804792
4793 if (power_user.trim_spaces) {
4794 reasoning = reasoning.trim();
4795 }
4796
4781 if (isContinue) {4797 if (isContinue) {
4782 getMessage = continue_mag + getMessage;4798 getMessage = continue_mag + getMessage;
4783 }4799 }
public/scripts/power-user.js+1 -0
@@ -254,6 +254,7 @@ let power_user = {
254 },254 },
255255
256 reasoning: {256 reasoning: {
257 auto_parse: false,
257 add_to_prompts: false,258 add_to_prompts: false,
258 prefix: '<think>\n',259 prefix: '<think>\n',
259 suffix: '\n</think>',260 suffix: '\n</think>',
public/scripts/reasoning.js+143 -2
@@ -1,4 +1,4 @@
1import { chat, closeMessageEditor, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';1import { chat, closeMessageEditor, event_types, eventSource, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
2import { getRegexedString, regex_placement } from './extensions/regex/engine.js';2import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
3import { t } from './i18n.js';3import { t } from './i18n.js';
4import { MacrosParser } from './macros.js';4import { MacrosParser } from './macros.js';
@@ -8,7 +8,7 @@ import { SlashCommand } from './slash-commands/SlashCommand.js';
8import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';8import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
9import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';9import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
10import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';10import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
11import { copyText } from './utils.js';11import { copyText, escapeRegex, isFalseBoolean } from './utils.js';
1212
13/**13/**
14 * Gets a message from a jQuery element.14 * Gets a message from a jQuery element.
@@ -106,6 +106,12 @@ function loadReasoningSettings() {
106 power_user.reasoning.max_additions = Number($(this).val());106 power_user.reasoning.max_additions = Number($(this).val());
107 saveSettingsDebounced();107 saveSettingsDebounced();
108 });108 });
109
110 $('#reasoning_auto_parse').prop('checked', power_user.reasoning.auto_parse);
111 $('#reasoning_auto_parse').on('change', function () {
112 power_user.reasoning.auto_parse = !!$(this).prop('checked');
113 saveSettingsDebounced();
114 });
109}115}
110116
111function registerReasoningSlashCommands() {117function registerReasoningSlashCommands() {
@@ -161,6 +167,49 @@ function registerReasoningSlashCommands() {
161 return message.extra.reasoning;167 return message.extra.reasoning;
162 },168 },
163 }));169 }));
170
171 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
172 name: 'reasoning-parse',
173 returns: 'reasoning string',
174 helpString: t`Extracts the reasoning block from a string using the Reasoning Formatting settings.`,
175 namedArgumentList: [
176 SlashCommandNamedArgument.fromProps({
177 name: 'regex',
178 description: 'Whether to apply regex scripts to the reasoning content.',
179 typeList: [ARGUMENT_TYPE.BOOLEAN],
180 defaultValue: 'true',
181 isRequired: false,
182 enumProvider: commonEnumProviders.boolean('trueFalse'),
183 }),
184 ],
185 unnamedArgumentList: [
186 SlashCommandArgument.fromProps({
187 description: 'input string',
188 typeList: [ARGUMENT_TYPE.STRING],
189 }),
190 ],
191 callback: (args, value) => {
192 if (!value) {
193 return '';
194 }
195
196 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
197 toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`);
198 return String(value);
199 }
200
201 const parsedReasoning = parseReasoningFromString(String(value));
202
203 if (!parsedReasoning) {
204 return '';
205 }
206
207 const applyRegex = !isFalseBoolean(String(args.regex ?? ''));
208 return applyRegex
209 ? getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING)
210 : parsedReasoning.reasoning;
211 },
212 }));
164}213}
165214
166function registerReasoningMacros() {215function registerReasoningMacros() {
@@ -290,9 +339,101 @@ function setReasoningEventHandlers(){
290 });339 });
291}340}
292341
342/**
343 * Parses reasoning from a string using the power user reasoning settings.
344 * @typedef {Object} ParsedReasoning
345 * @property {string} reasoning Reasoning block
346 * @property {string} content Message content
347 * @param {string} str Content of the message
348 * @returns {ParsedReasoning|null} Parsed reasoning block and message content
349 */
350function parseReasoningFromString(str) {
351 // Both prefix and suffix must be defined
352 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
353 return null;
354 }
355
356 try {
357 const regex = new RegExp(`${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');
358
359 let didReplace = false;
360 let reasoning = '';
361 let content = String(str).replace(regex, (_match, captureGroup) => {
362 didReplace = true;
363 reasoning = captureGroup;
364 return '';
365 });
366
367 if (didReplace && power_user.trim_spaces) {
368 reasoning = reasoning.trim();
369 content = content.trim();
370 }
371
372 return { reasoning, content };
373 } catch (error) {
374 console.error('[Reasoning] Error parsing reasoning block', error);
375 return null;
376 }
377}
378
379function registerReasoningAppEvents() {
380 eventSource.makeFirst(event_types.MESSAGE_RECEIVED, (/** @type {number} */ idx) => {
381 if (!power_user.reasoning.auto_parse) {
382 return;
383 }
384
385 console.debug('[Reasoning] Auto-parsing reasoning block for message', idx);
386 const message = chat[idx];
387
388 if (!message) {
389 console.warn('[Reasoning] Message not found', idx);
390 return null;
391 }
392
393 if (!message.mes || message.mes === '...') {
394 console.debug('[Reasoning] Message content is empty or a placeholder', idx);
395 return null;
396 }
397
398 const parsedReasoning = parseReasoningFromString(message.mes);
399
400 // No reasoning block found
401 if (!parsedReasoning) {
402 return;
403 }
404
405 // Make sure the message has an extra object
406 if (!message.extra || typeof message.extra !== 'object') {
407 message.extra = {};
408 }
409
410 const contentUpdated = !!parsedReasoning.reasoning || parsedReasoning.content !== message.mes;
411
412 // If reasoning was found, add it to the message
413 if (parsedReasoning.reasoning) {
414 message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING);
415 }
416
417 // Update the message text if it was changed
418 if (parsedReasoning.content !== message.mes) {
419 message.mes = parsedReasoning.content;
420 }
421
422 // Find if a message already exists in DOM and must be updated
423 if (contentUpdated) {
424 const messageRendered = document.querySelector(`.mes[mesid="${idx}"]`) !== null;
425 if (messageRendered) {
426 console.debug('[Reasoning] Updating message block', idx);
427 updateMessageBlock(idx, message);
428 }
429 }
430 });
431}
432
293export function initReasoning() {433export function initReasoning() {
294 loadReasoningSettings();434 loadReasoningSettings();
295 setReasoningEventHandlers();435 setReasoningEventHandlers();
296 registerReasoningSlashCommands();436 registerReasoningSlashCommands();
297 registerReasoningMacros();437 registerReasoningMacros();
438 registerReasoningAppEvents();
298}439}