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 -3Ignore whitespace
public/css/toggle-dependent.css+5 -0
@@ -472,6 +472,11 @@ label[for="trim_spaces"]:has(input:checked) i.warning {
472472 display: none;
473473}
474474
475+label[for="trim_spaces"]:not(:has(input:checked)) small {
476+ color: var(--warning);
477+ opacity: 1;
478+}
479+
475480#claude_function_prefill_warning {
476481 display: none;
477482 color: red;
public/index.html+6 -0
@@ -3791,6 +3791,12 @@
37913791 <span data-i18n="Reasoning">Reasoning</span>
37923792 </h4>
37933793 <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>
37943800 <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">
37953801 <input id="reasoning_add_to_prompts" type="checkbox" />
37963802 <small data-i18n="Add Reasoning to Prompts">
public/script.js+17 -1
@@ -170,6 +170,7 @@ import {
170170 toggleDrawer,
171171 isElementInViewport,
172172 copyText,
173+ escapeHtml,
173174} from './scripts/utils.js';
174175import { debounce_timeout } from './scripts/constants.js';
175176
@@ -2067,6 +2068,17 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
20672068 mes = mes.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
20682069 }
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+
20702082 if (!isSystem) {
20712083 // Save double quotes in tags as a special character to prevent them from being encoded
20722084 if (!power_user.encode_tags) {
@@ -3209,7 +3221,7 @@ class StreamingProcessor {
32093221 }
32103222
32113223 if (this.reasoning) {
32123224 chat[messageId]['extra']['reasoning'] = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
32133225 if (this.messageReasoningDom instanceof HTMLElement) {
32143226 const formattedReasoning = messageFormatting(this.reasoning, '', false, false, messageId, {}, true);
32153227 this.messageReasoningDom.innerHTML = formattedReasoning;
@@ -4778,6 +4790,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
47784790 messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);
47794791 reasoning = getRegexedString(reasoning, regex_placement.REASONING);
47804792
4793+ if (power_user.trim_spaces) {
4794+ reasoning = reasoning.trim();
4795+ }
4796+
47814797 if (isContinue) {
47824798 getMessage = continue_mag + getMessage;
47834799 }
public/scripts/power-user.js+1 -0
@@ -254,6 +254,7 @@ let power_user = {
254254 },
255255
256256 reasoning: {
257+ auto_parse: false,
257258 add_to_prompts: false,
258259 prefix: '<think>\n',
259260 suffix: '\n</think>',
public/scripts/reasoning.js+143 -2
@@ -1,4 +1,4 @@
11import { chat, closeMessageEditor, event_types, eventSource, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
22import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
33import { t } from './i18n.js';
44import { MacrosParser } from './macros.js';
@@ -8,7 +8,7 @@ import { SlashCommand } from './slash-commands/SlashCommand.js';
88import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
99import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
1010import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
1111import { copyText, escapeRegex, isFalseBoolean } from './utils.js';
1212
1313/**
1414 * Gets a message from a jQuery element.
@@ -106,6 +106,12 @@ function loadReasoningSettings() {
106106 power_user.reasoning.max_additions = Number($(this).val());
107107 saveSettingsDebounced();
108108 });
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+ });
109115}
110116
111117function registerReasoningSlashCommands() {
@@ -161,6 +167,49 @@ function registerReasoningSlashCommands() {
161167 return message.extra.reasoning;
162168 },
163169 }));
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+ }));
164213}
165214
166215function registerReasoningMacros() {
@@ -290,9 +339,101 @@ function setReasoningEventHandlers(){
290339 });
291340}
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+ */
350+function 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+
379+function 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+
293433export function initReasoning() {
294434 loadReasoningSettings();
295435 setReasoningEventHandlers();
296436 registerReasoningSlashCommands();
297437 registerReasoningMacros();
438+ registerReasoningAppEvents();
298439}