Add reasoning auto-parsing, always show reasoning prefix/suffix in Markdown, respect space trim preference
| @@ -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"> |
| @@ -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'; |
| 173 | import { debounce_timeout } from './scripts/constants.js'; | 174 | import { debounce_timeout } from './scripts/constants.js'; |
| 174 | 175 | ||
| @@ -2066,6 +2067,17 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san | |||
| 2066 | mes = mes.replaceAll('<', '<').replaceAll('>', '>'); | 2067 | mes = mes.replaceAll('<', '<').replaceAll('>', '>'); |
| 2067 | } | 2068 | } |
| 2068 | 2069 | ||
| 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 encoded | 2082 | // 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 | } |
| 3209 | 3221 | ||
| 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); |
| 4807 | 4819 | ||
| 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 | } |
| @@ -254,6 +254,7 @@ let power_user = { | |||
| 254 | }, | 254 | }, |
| 255 | 255 | ||
| 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>', |
| @@ -1,4 +1,4 @@ | |||
| 1 | import { chat, closeMessageEditor, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js'; | 1 | import { chat, closeMessageEditor, event_types, eventSource, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js'; |
| 2 | import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; | 2 | import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; |
| 3 | import { t } from './i18n.js'; | 3 | import { t } from './i18n.js'; |
| 4 | import { MacrosParser } from './macros.js'; | 4 | import { MacrosParser } from './macros.js'; |
| @@ -8,7 +8,7 @@ import { SlashCommand } from './slash-commands/SlashCommand.js'; | |||
| 8 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; | 8 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; |
| 9 | import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; | 9 | import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 10 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; | 10 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 11 | import { copyText } from './utils.js'; | 11 | import { copyText, escapeRegex } from './utils.js'; |
| 12 | 12 | ||
| 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 | } |
| 110 | 116 | ||
| 111 | function registerReasoningSlashCommands() { | 117 | function registerReasoningSlashCommands() { |
| @@ -290,9 +296,94 @@ function setReasoningEventHandlers(){ | |||
| 290 | }); | 296 | }); |
| 291 | } | 297 | } |
| 292 | 298 | ||
| 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 | */ | ||
| 307 | function 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 | |||
| 329 | function 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 | |||
| 293 | export function initReasoning() { | 383 | export function initReasoning() { |
| 294 | loadReasoningSettings(); | 384 | loadReasoningSettings(); |
| 295 | setReasoningEventHandlers(); | 385 | setReasoningEventHandlers(); |
| 296 | registerReasoningSlashCommands(); | 386 | registerReasoningSlashCommands(); |
| 297 | registerReasoningMacros(); | 387 | registerReasoningMacros(); |
| 388 | registerReasoningAppEvents(); | ||
| 298 | } | 389 | } |