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 | // Update reasoning | 3224 | // Update reasoning |
| 3225 | await this.reasoningHandler.process(messageId, mesChanged); | 3225 | await this.reasoningHandler.process(messageId, mesChanged); |
| 3226 | processedText = chat[messageId]['mes']; | ||
| 3226 | 3227 | ||
| 3227 | // Token count update. | 3228 | // Token count update. |
| 3228 | const tokenCountText = this.reasoningHandler.reasoning + processedText; | 3229 | const tokenCountText = this.reasoningHandler.reasoning + processedText; |
| @@ -3373,7 +3374,7 @@ class StreamingProcessor { | |||
| 3373 | this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs])); | 3374 | this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs])); |
| 3374 | } | 3375 | } |
| 3375 | // Get the updated reasoning string into the handler | 3376 | // Get the updated reasoning string into the handler |
| 3376 | this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning ?? ''); | 3377 | this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning); |
| 3377 | await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text); | 3378 | await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text); |
| 3378 | await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text)); | 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 | import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; | 13 | import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 14 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; | 14 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 15 | import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; | 15 | import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; |
| 16 | import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty } from './utils.js'; | 16 | import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty, trimSpaces } from './utils.js'; |
| 17 | 17 | ||
| 18 | /** | 18 | /** |
| 19 | * Gets a message from a jQuery element. | 19 | * Gets a message from a jQuery element. |
| @@ -129,7 +129,12 @@ export const ReasoningState = { | |||
| 129 | * This class is used inside the {@link StreamingProcessor} to manage reasoning states and UI updates. | 129 | * This class is used inside the {@link StreamingProcessor} to manage reasoning states and UI updates. |
| 130 | */ | 130 | */ |
| 131 | export class ReasoningHandler { | 131 | export class ReasoningHandler { |
| 132 | /** @type {boolean} True if the model supports reasoning, but hides the reasoning output */ | ||
| 132 | #isHiddenReasoningModel; | 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 | * @param {Date?} [timeStarted=null] - When the generation started | 140 | * @param {Date?} [timeStarted=null] - When the generation started |
| @@ -147,7 +152,6 @@ export class ReasoningHandler { | |||
| 147 | /** @type {Date} Initial starting time of the generation */ | 152 | /** @type {Date} Initial starting time of the generation */ |
| 148 | this.initialTime = timeStarted ?? new Date(); | 153 | this.initialTime = timeStarted ?? new Date(); |
| 149 | 154 | ||
| 150 | /** @type {boolean} True if the model supports reasoning, but hides the reasoning output */ | ||
| 151 | this.#isHiddenReasoningModel = isHiddenReasoningModel(); | 155 | this.#isHiddenReasoningModel = isHiddenReasoningModel(); |
| 152 | 156 | ||
| 153 | // Cached DOM elements for reasoning | 157 | // Cached DOM elements for reasoning |
| @@ -237,18 +241,19 @@ export class ReasoningHandler { | |||
| 237 | * Updates the reasoning text/string for a message. | 241 | * Updates the reasoning text/string for a message. |
| 238 | * | 242 | * |
| 239 | * @param {number} messageId - The ID of the message to update | 243 | * @param {number} messageId - The ID of the message to update |
| 240 | * @param {string?} [reasoning=null] - The reasoning text to update - If null, uses the current reasoning | 244 | * @param {string?} [reasoning=null] - The reasoning text to update - If null or empty, uses the current reasoning |
| 241 | * @param {Object} [options={}] - Optional arguments | 245 | * @param {Object} [options={}] - Optional arguments |
| 242 | * @param {boolean} [options.persist=false] - Whether to persist the reasoning to the message object | 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 | * @returns {boolean} - Returns true if the reasoning was changed, otherwise false | 248 | * @returns {boolean} - Returns true if the reasoning was changed, otherwise false |
| 244 | */ | 249 | */ |
| 245 | updateReasoning(messageId, reasoning = null, { persist = false } = {}) { | 250 | updateReasoning(messageId, reasoning = null, { persist = false, allowReset = false } = {}) { |
| 246 | if (messageId == -1 || !chat[messageId]) { | 251 | if (messageId == -1 || !chat[messageId]) { |
| 247 | return false; | 252 | return false; |
| 248 | } | 253 | } |
| 249 | 254 | ||
| 250 | reasoning = reasoning ?? this.reasoning; | 255 | reasoning = allowReset ? reasoning ?? this.reasoning : reasoning || this.reasoning; |
| 251 | reasoning = power_user.trim_spaces ? reasoning.trim() : reasoning; | 256 | reasoning = trimSpaces(reasoning); |
| 252 | 257 | ||
| 253 | // Ensure the chat extra exists | 258 | // Ensure the chat extra exists |
| 254 | if (!chat[messageId].extra) { | 259 | if (!chat[messageId].extra) { |
| @@ -279,7 +284,10 @@ export class ReasoningHandler { | |||
| 279 | * @returns {Promise<void>} | 284 | * @returns {Promise<void>} |
| 280 | */ | 285 | */ |
| 281 | async process(messageId, mesChanged) { | 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 | // Ensure reasoning string is updated and regexes are applied correctly | 292 | // Ensure reasoning string is updated and regexes are applied correctly |
| 285 | const reasoningChanged = this.updateReasoning(messageId, null, { persist: true }); | 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 | * Completes the reasoning process for a message. | 353 | * Completes the reasoning process for a message. |
| 299 | * | 354 | * |
| @@ -338,7 +393,7 @@ export class ReasoningHandler { | |||
| 338 | setDatasetProperty(this.messageReasoningDetailsDom, 'state', this.state); | 393 | setDatasetProperty(this.messageReasoningDetailsDom, 'state', this.state); |
| 339 | 394 | ||
| 340 | // Update the reasoning message | 395 | // Update the reasoning message |
| 341 | const reasoning = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning; | 396 | const reasoning = trimSpaces(this.reasoning); |
| 342 | const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true); | 397 | const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true); |
| 343 | this.messageReasoningContentDom.innerHTML = displayReasoning; | 398 | this.messageReasoningContentDom.innerHTML = displayReasoning; |
| 344 | 399 | ||
| @@ -838,9 +893,9 @@ function parseReasoningFromString(str) { | |||
| 838 | return ''; | 893 | return ''; |
| 839 | }); | 894 | }); |
| 840 | 895 | ||
| 841 | if (didReplace && power_user.trim_spaces) { | 896 | if (didReplace) { |
| 842 | reasoning = reasoning.trim(); | 897 | reasoning = trimSpaces(reasoning); |
| 843 | content = content.trim(); | 898 | content = trimSpaces(content); |
| 844 | } | 899 | } |
| 845 | 900 | ||
| 846 | return { reasoning, content }; | 901 | return { reasoning, content }; |
| @@ -8,7 +8,7 @@ import { | |||
| 8 | import { getContext } from './extensions.js'; | 8 | import { getContext } from './extensions.js'; |
| 9 | import { characters, getRequestHeaders, this_chid } from '../script.js'; | 9 | import { characters, getRequestHeaders, this_chid } from '../script.js'; |
| 10 | import { isMobile } from './RossAscends-mods.js'; | 10 | import { isMobile } from './RossAscends-mods.js'; |
| 11 | import { collapseNewlines } from './power-user.js'; | 11 | import { collapseNewlines, power_user } from './power-user.js'; |
| 12 | import { debounce_timeout } from './constants.js'; | 12 | import { debounce_timeout } from './constants.js'; |
| 13 | import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; | 13 | import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; |
| 14 | import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; | 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 | * Trims a string to the end of a nearest sentence. | 693 | * Trims a string to the end of a nearest sentence. |
| 681 | * @param {string} input The string to trim. | 694 | * @param {string} input The string to trim. |
| 682 | * @returns {string} The trimmed string. | 695 | * @returns {string} The trimmed string. |