Handle auto parsing reasoning during streaming - Add function to handle auto parsing reasoning from the streamed message during streaming - Only works when the reasoning prefix is EXACTLY at the beginning of the message - Tried to keep this lightweight, no regex parsing, remembering the index, so it's simple string splicing - Add utility function that trims a string only if `trim_spaces` is enabled
| @@ -3223,6 +3223,7 @@ class StreamingProcessor { | ||
| 3223 | 3223 | |
| 3224 | 3224 | // Update reasoning |
| 3225 | 3225 | await this.reasoningHandler.process(messageId, mesChanged); |
| 3226 | + processedText = chat[messageId]['mes']; | |
| 3226 | 3227 | |
| 3227 | 3228 | // Token count update. |
| 3228 | 3229 | const tokenCountText = this.reasoningHandler.reasoning + processedText; |
| @@ -3373,7 +3374,7 @@ class StreamingProcessor { | ||
| 3373 | 3374 | this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs])); |
| 3374 | 3375 | } |
| 3375 | 3376 | // Get the updated reasoning string into the handler |
| 3376 | 3377 | this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning ?? ''); |
| 3377 | 3378 | await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text); |
| 3378 | 3379 | await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text)); |
| 3379 | 3380 | } |
| @@ -13,7 +13,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from ' | ||
| 13 | 13 | import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 14 | 14 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 15 | 15 | import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; |
| 16 | 16 | import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty, trimSpaces } from './utils.js'; |
| 17 | 17 | |
| 18 | 18 | /** |
| 19 | 19 | * Gets a message from a jQuery element. |
| @@ -129,7 +129,12 @@ export const ReasoningState = { | ||
| 129 | 129 | * This class is used inside the {@link StreamingProcessor} to manage reasoning states and UI updates. |
| 130 | 130 | */ |
| 131 | 131 | export class ReasoningHandler { |
| 132 | + /** @type {boolean} True if the model supports reasoning, but hides the reasoning output */ | |
| 132 | 133 | #isHiddenReasoningModel; |
| 134 | + /** @type {boolean} True if the handler is currently handling a manual parse of reasoning blocks */ | |
| 135 | + #isParsingReasoning = false; | |
| 136 | + /** @type {number?} When reasoning is being parsed manually, and the reasoning has ended, this will be the index at which the actual messages starts */ | |
| 137 | + #parsingReasoningMesStartIndex = null; | |
| 133 | 138 | |
| 134 | 139 | /** |
| 135 | 140 | * @param {Date?} [timeStarted=null] - When the generation started |
| @@ -147,7 +152,6 @@ export class ReasoningHandler { | ||
| 147 | 152 | /** @type {Date} Initial starting time of the generation */ |
| 148 | 153 | this.initialTime = timeStarted ?? new Date(); |
| 149 | 154 | |
| 150 | - /** @type {boolean} True if the model supports reasoning, but hides the reasoning output */ | |
| 151 | 155 | this.#isHiddenReasoningModel = isHiddenReasoningModel(); |
| 152 | 156 | |
| 153 | 157 | // Cached DOM elements for reasoning |
| @@ -237,18 +241,19 @@ export class ReasoningHandler { | ||
| 237 | 241 | * Updates the reasoning text/string for a message. |
| 238 | 242 | * |
| 239 | 243 | * @param {number} messageId - The ID of the message to update |
| 240 | 244 | * @param {string?} [reasoning=null] - The reasoning text to update - If null or empty, uses the current reasoning |
| 241 | 245 | * @param {Object} [options={}] - Optional arguments |
| 242 | 246 | * @param {boolean} [options.persist=false] - Whether to persist the reasoning to the message object |
| 247 | + * @param {boolean} [options.allowReset=false] - Whether to allow empty reasoning provided to reset the reasoning, instead of just taking the existing one | |
| 243 | 248 | * @returns {boolean} - Returns true if the reasoning was changed, otherwise false |
| 244 | 249 | */ |
| 245 | 250 | updateReasoning(messageId, reasoning = null, { persist = false, allowReset = false } = {}) { |
| 246 | 251 | if (messageId == -1 || !chat[messageId]) { |
| 247 | 252 | return false; |
| 248 | 253 | } |
| 249 | 254 | |
| 250 | 255 | reasoning = allowReset ? reasoning ?? this.reasoning : reasoning || this.reasoning; |
| 251 | 256 | reasoning = power_user.trim_spaces ? reasoning.trimtrimSpaces() : reasoning); |
| 252 | 257 | |
| 253 | 258 | // Ensure the chat extra exists |
| 254 | 259 | if (!chat[messageId].extra) { |
| @@ -279,7 +284,10 @@ export class ReasoningHandler { | ||
| 279 | 284 | * @returns {Promise<void>} |
| 280 | 285 | */ |
| 281 | 286 | async process(messageId, mesChanged) { |
| 282 | - if (!this.reasoning && !this.#isHiddenReasoningModel) return; | |
| 287 | + mesChanged = this.#autoParseReasoningFromMessage(messageId, mesChanged); | |
| 288 | + | |
| 289 | + if (!this.reasoning && !this.#isHiddenReasoningModel) | |
| 290 | + return; | |
| 283 | 291 | |
| 284 | 292 | // Ensure reasoning string is updated and regexes are applied correctly |
| 285 | 293 | const reasoningChanged = this.updateReasoning(messageId, null, { persist: true }); |
| @@ -294,6 +302,53 @@ export class ReasoningHandler { | ||
| 294 | 302 | } |
| 295 | 303 | } |
| 296 | 304 | |
| 305 | + #autoParseReasoningFromMessage(messageId, mesChanged) { | |
| 306 | + if (!power_user.reasoning.auto_parse) | |
| 307 | + return; | |
| 308 | + if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) | |
| 309 | + return mesChanged; | |
| 310 | + | |
| 311 | + /** @type {{ mes: string, [key: string]: any}} */ | |
| 312 | + const message = chat[messageId]; | |
| 313 | + if (!message) return mesChanged; | |
| 314 | + | |
| 315 | + // If we are done with reasoning parse, we just split the message correctly so the reasoning doesn't show up inside of it. | |
| 316 | + if (this.#parsingReasoningMesStartIndex) { | |
| 317 | + message.mes = trimSpaces(message.mes.slice(this.#parsingReasoningMesStartIndex)); | |
| 318 | + return mesChanged; | |
| 319 | + } | |
| 320 | + | |
| 321 | + if (this.state === ReasoningState.None) { | |
| 322 | + // If streamed message starts with the opening, cut it out and put all inside reasoning | |
| 323 | + if (message.mes.startsWith(power_user.reasoning.prefix) && message.mes.length > power_user.reasoning.prefix.length) { | |
| 324 | + this.#isParsingReasoning = true; | |
| 325 | + | |
| 326 | + // Manually set starting state here, as we might already have received the ending suffix | |
| 327 | + this.state = ReasoningState.Thinking; | |
| 328 | + this.startTime = this.initialTime; | |
| 329 | + } | |
| 330 | + } | |
| 331 | + | |
| 332 | + if (!this.#isParsingReasoning) | |
| 333 | + return mesChanged; | |
| 334 | + | |
| 335 | + // If we are in manual parsing mode, all currently streaming mes tokens will go the the reasoning block | |
| 336 | + const originalMes = message.mes; | |
| 337 | + this.reasoning = originalMes.slice(power_user.reasoning.prefix.length); | |
| 338 | + message.mes = ''; | |
| 339 | + | |
| 340 | + // If the reasoning contains the ending suffix, we cut that off and continue as message streaming | |
| 341 | + if (this.reasoning.includes(power_user.reasoning.suffix)) { | |
| 342 | + this.reasoning = this.reasoning.slice(0, this.reasoning.indexOf(power_user.reasoning.suffix)); | |
| 343 | + this.#parsingReasoningMesStartIndex = originalMes.indexOf(power_user.reasoning.suffix) + power_user.reasoning.suffix.length; | |
| 344 | + message.mes = trimSpaces(originalMes.slice(this.#parsingReasoningMesStartIndex)); | |
| 345 | + this.#isParsingReasoning = false; | |
| 346 | + } | |
| 347 | + | |
| 348 | + // Only return the original mesChanged value if we haven't cut off the complete message | |
| 349 | + return message.mes.length ? mesChanged : false; | |
| 350 | + } | |
| 351 | + | |
| 297 | 352 | /** |
| 298 | 353 | * Completes the reasoning process for a message. |
| 299 | 354 | * |
| @@ -338,7 +393,7 @@ export class ReasoningHandler { | ||
| 338 | 393 | setDatasetProperty(this.messageReasoningDetailsDom, 'state', this.state); |
| 339 | 394 | |
| 340 | 395 | // Update the reasoning message |
| 341 | 396 | const reasoning = power_user.trim_spaces ? this.reasoning.trimtrimSpaces() : this.reasoning); |
| 342 | 397 | const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true); |
| 343 | 398 | this.messageReasoningContentDom.innerHTML = displayReasoning; |
| 344 | 399 | |
| @@ -838,9 +893,9 @@ function parseReasoningFromString(str) { | ||
| 838 | 893 | return ''; |
| 839 | 894 | }); |
| 840 | 895 | |
| 841 | 896 | if (didReplace && power_user.trim_spaces) { |
| 842 | 897 | reasoning = reasoning.trimtrimSpaces(reasoning); |
| 843 | 898 | content = content.trimtrimSpaces(content); |
| 844 | 899 | } |
| 845 | 900 | |
| 846 | 901 | return { reasoning, content }; |
| @@ -8,7 +8,7 @@ import { | ||
| 8 | 8 | import { getContext } from './extensions.js'; |
| 9 | 9 | import { characters, getRequestHeaders, this_chid } from '../script.js'; |
| 10 | 10 | import { isMobile } from './RossAscends-mods.js'; |
| 11 | 11 | import { collapseNewlines, power_user } from './power-user.js'; |
| 12 | 12 | import { debounce_timeout } from './constants.js'; |
| 13 | 13 | import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; |
| 14 | 14 | import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; |
| @@ -677,6 +677,19 @@ export function sortByCssOrder(a, b) { | ||
| 677 | 677 | } |
| 678 | 678 | |
| 679 | 679 | /** |
| 680 | + * Trims leading and trailing whitespace from the input string based on a configuration setting. | |
| 681 | + * @param {string} input - The string to be trimmed | |
| 682 | + * @returns {string} The trimmed string if trimming is enabled; otherwise, returns the original string | |
| 683 | + */ | |
| 684 | + | |
| 685 | +export function trimSpaces(input) { | |
| 686 | + if (!input || typeof input !== 'string') { | |
| 687 | + return input; | |
| 688 | + } | |
| 689 | + return power_user.trim_spaces ? input.trim() : input; | |
| 690 | +} | |
| 691 | + | |
| 692 | +/** | |
| 680 | 693 | * Trims a string to the end of a nearest sentence. |
| 681 | 694 | * @param {string} input The string to trim. |
| 682 | 695 | * @returns {string} The trimmed string. |