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

9590127bae282e31bcca5fe5c475a4426a080bc0

Wolfsblvt <wolfsblvt@gmail.com>

3 files changed, +82 -13Ignore whitespace
public/script.js+2 -1
@@ -3223,6 +3223,7 @@ class StreamingProcessor {
32233223
3224 // Update reasoning3224 // Update reasoning
3225 await this.reasoningHandler.process(messageId, mesChanged);3225 await this.reasoningHandler.process(messageId, mesChanged);
3226 processedText = chat[messageId]['mes'];
32263227
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 handler3376 // 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 }
public/scripts/reasoning.js+66 -11
@@ -13,7 +13,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
13import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';13import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
14import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';14import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
15import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';15import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
16import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty } from './utils.js';16import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty, trimSpaces } from './utils.js';
1717
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 */
131export class ReasoningHandler {131export 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;
133138
134 /**139 /**
135 * @param {Date?} [timeStarted=null] - When the generation started140 * @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();
149154
150 /** @type {boolean} True if the model supports reasoning, but hides the reasoning output */
151 this.#isHiddenReasoningModel = isHiddenReasoningModel();155 this.#isHiddenReasoningModel = isHiddenReasoningModel();
152156
153 // Cached DOM elements for reasoning157 // 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 update243 * @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 reasoning244 * @param {string?} [reasoning=null] - The reasoning text to update - If null or empty, uses the current reasoning
241 * @param {Object} [options={}] - Optional arguments245 * @param {Object} [options={}] - Optional arguments
242 * @param {boolean} [options.persist=false] - Whether to persist the reasoning to the message object246 * @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 false248 * @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 }
249254
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);
252257
253 // Ensure the chat extra exists258 // 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;
283291
284 // Ensure reasoning string is updated and regexes are applied correctly292 // 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 }
296304
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);
339394
340 // Update the reasoning message395 // 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;
344399
@@ -838,9 +893,9 @@ function parseReasoningFromString(str) {
838 return '';893 return '';
839 });894 });
840895
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 }
845900
846 return { reasoning, content };901 return { reasoning, content };
public/scripts/utils.js+14 -1
@@ -8,7 +8,7 @@ import {
8import { getContext } from './extensions.js';8import { getContext } from './extensions.js';
9import { characters, getRequestHeaders, this_chid } from '../script.js';9import { characters, getRequestHeaders, this_chid } from '../script.js';
10import { isMobile } from './RossAscends-mods.js';10import { isMobile } from './RossAscends-mods.js';
11import { collapseNewlines } from './power-user.js';11import { collapseNewlines, power_user } from './power-user.js';
12import { debounce_timeout } from './constants.js';12import { debounce_timeout } from './constants.js';
13import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';13import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
14import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';14import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -677,6 +677,19 @@ export function sortByCssOrder(a, b) {
677}677}
678678
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
685export 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.