Add reasoning auto-parsing, always show reasoning prefix/suffix in Markdown, respect space trim preference

d0abba23dca061bb1875a2bb62319810a1f70976

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

4 files changed, +117 -3Showing whitespace changes
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
@@ -169,6 +169,7 @@ import {
169 toggleDrawer,169 toggleDrawer,
170 isElementInViewport,170 isElementInViewport,
171 copyText,171 copyText,
172 escapeHtml,
172} from './scripts/utils.js';173} from './scripts/utils.js';
173import { debounce_timeout } from './scripts/constants.js';174import { debounce_timeout } from './scripts/constants.js';
174175
@@ -2066,6 +2067,17 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
2066 mes = mes.replaceAll('<', '&lt;').replaceAll('>', '&gt;');2067 mes = mes.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
2067 }2068 }
20682069
2070 // Make sure reasoning strings are always shown, even if they include "<" or ">"
2071 [power_user.reasoning.prefix, power_user.reasoning.suffix].forEach((reasoningString) => {
2072 if (!reasoningString || !reasoningString.trim().length) {
2073 return;
2074 }
2075 // Only replace the first occurrence of the reasoning string
2076 if (mes.includes(reasoningString)) {
2077 mes = mes.replace(reasoningString, escapeHtml(reasoningString));
2078 }
2079 });
2080
2069 if (!isSystem) {2081 if (!isSystem) {
2070 // Save double quotes in tags as a special character to prevent them from being encoded2082 // Save double quotes in tags as a special character to prevent them from being encoded
2071 if (!power_user.encode_tags) {2083 if (!power_user.encode_tags) {
@@ -3208,7 +3220,7 @@ class StreamingProcessor {
3208 }3220 }
32093221
3210 if (this.reasoning) {3222 if (this.reasoning) {
3211 chat[messageId]['extra']['reasoning'] = this.reasoning;3223 chat[messageId]['extra']['reasoning'] = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
3212 if (this.messageReasoningDom instanceof HTMLElement) {3224 if (this.messageReasoningDom instanceof HTMLElement) {
3213 const formattedReasoning = messageFormatting(this.reasoning, '', false, false, messageId, {}, true);3225 const formattedReasoning = messageFormatting(this.reasoning, '', false, false, messageId, {}, true);
3214 this.messageReasoningDom.innerHTML = formattedReasoning;3226 this.messageReasoningDom.innerHTML = formattedReasoning;
@@ -4805,6 +4817,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4805 messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);4817 messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);
4806 reasoning = getRegexedString(reasoning, regex_placement.REASONING);4818 reasoning = getRegexedString(reasoning, regex_placement.REASONING);
48074819
4820 if (power_user.trim_spaces) {
4821 reasoning = reasoning.trim();
4822 }
4823
4808 if (isContinue) {4824 if (isContinue) {
4809 getMessage = continue_mag + getMessage;4825 getMessage = continue_mag + getMessage;
4810 }4826 }
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+93 -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 } 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() {
@@ -290,9 +296,94 @@ function setReasoningEventHandlers(){
290 });296 });
291}297}
292298
299/**
300 * Parses reasoning from a string using the power user reasoning settings.
301 * @typedef {Object} ParsedReasoning
302 * @property {string} reasoning Reasoning block
303 * @property {string} content Message content
304 * @param {string} str Content of the message
305 * @returns {ParsedReasoning|null} Parsed reasoning block and message content
306 */
307function parseReasoningFromString(str) {
308 // Both prefix and suffix must be defined
309 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
310 return null;
311 }
312
313 try {
314 let reasoning = '';
315
316 const regex = new RegExp(`${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');
317 const content = String(str).replace(regex, (_match, captureGroup) => {
318 reasoning = power_user.trim_spaces ? captureGroup.trim() : captureGroup;
319 return '';
320 });
321
322 return { reasoning, content };
323 } catch (error) {
324 console.error('[Reasoning] Error parsing reasoning block', error);
325 return null;
326 }
327}
328
329function registerReasoningAppEvents() {
330 eventSource.makeFirst(event_types.MESSAGE_RECEIVED, (/** @type {number} */ idx) => {
331 if (!power_user.reasoning.auto_parse) {
332 return;
333 }
334
335 console.debug('[Reasoning] Auto-parsing reasoning block for message', idx);
336 const message = chat[idx];
337
338 if (!message) {
339 console.warn('[Reasoning] Message not found', idx);
340 return null;
341 }
342
343 if (!message.mes || message.mes === '...') {
344 console.debug('[Reasoning] Message content is empty or a placeholder', idx);
345 return null;
346 }
347
348 const parsedReasoning = parseReasoningFromString(message.mes);
349
350 // No reasoning block found
351 if (!parsedReasoning) {
352 return;
353 }
354
355 // Make sure the message has an extra object
356 if (!message.extra || typeof message.extra !== 'object') {
357 message.extra = {};
358 }
359
360 const contentUpdated = !!parsedReasoning.reasoning || parsedReasoning.content !== message.mes;
361
362 // If reasoning was found, add it to the message
363 if (parsedReasoning.reasoning) {
364 message.extra.reasoning = parsedReasoning.reasoning;
365 }
366
367 // Update the message text if it was changed
368 if (parsedReasoning.content !== message.mes) {
369 message.mes = parsedReasoning.content;
370 }
371
372 // Find if a message already exists in DOM and must be updated
373 if (contentUpdated) {
374 const messageRendered = document.querySelector(`.mes[mesid="${idx}"]`) !== null;
375 if (messageRendered) {
376 console.debug('[Reasoning] Updating message block', idx);
377 updateMessageBlock(idx, message);
378 }
379 }
380 });
381}
382
293export function initReasoning() {383export function initReasoning() {
294 loadReasoningSettings();384 loadReasoningSettings();
295 setReasoningEventHandlers();385 setReasoningEventHandlers();
296 registerReasoningSlashCommands();386 registerReasoningSlashCommands();
297 registerReasoningMacros();387 registerReasoningMacros();
388 registerReasoningAppEvents();
298}389}