Merge pull request #3386 from SillyTavern/reasoning-parse Reasoning blocks auto-parsing
Signed| @@ -472,6 +472,11 @@ label[for="trim_spaces"]:has(input:checked) i.warning { | |||
| 472 | display: none; | 472 | display: none; |
| 473 | } | 473 | } |
| 474 | 474 | ||
| 475 | label[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; |
| @@ -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"> |
| @@ -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'; |
| 174 | import { debounce_timeout } from './scripts/constants.js'; | 175 | import { debounce_timeout } from './scripts/constants.js'; |
| 175 | 176 | ||
| @@ -2067,6 +2068,17 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san | |||
| 2067 | mes = mes.replaceAll('<', '<').replaceAll('>', '>'); | 2068 | mes = mes.replaceAll('<', '<').replaceAll('>', '>'); |
| 2068 | } | 2069 | } |
| 2069 | 2070 | ||
| 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 encoded | 2083 | // 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 | } |
| 3210 | 3222 | ||
| 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); |
| 4780 | 4792 | ||
| 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 | } |
| @@ -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, isFalseBoolean } 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() { |
| @@ -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 | } |
| 165 | 214 | ||
| 166 | function registerReasoningMacros() { | 215 | function registerReasoningMacros() { |
| @@ -290,9 +339,101 @@ function setReasoningEventHandlers(){ | |||
| 290 | }); | 339 | }); |
| 291 | } | 340 | } |
| 292 | 341 | ||
| 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 | |||
| 293 | export function initReasoning() { | 433 | export function initReasoning() { |
| 294 | loadReasoningSettings(); | 434 | loadReasoningSettings(); |
| 295 | setReasoningEventHandlers(); | 435 | setReasoningEventHandlers(); |
| 296 | registerReasoningSlashCommands(); | 436 | registerReasoningSlashCommands(); |
| 297 | registerReasoningMacros(); | 437 | registerReasoningMacros(); |
| 438 | registerReasoningAppEvents(); | ||
| 298 | } | 439 | } |