Merge pull request #3634 from SillyTavern/continue-from-reasoning Fix auto-parsing of continue from reasoning
Signed| @@ -3204,13 +3204,21 @@ class StreamingProcessor { | |||
| 3204 | this.promptReasoning = promptReasoning; | 3204 | this.promptReasoning = promptReasoning; |
| 3205 | } | 3205 | } |
| 3206 | 3206 | ||
| 3207 | #checkDomElements(messageId) { | 3207 | /** |
| 3208 | * Initializes DOM elements for the current message. | ||
| 3209 | * @param {number} messageId Current message ID | ||
| 3210 | * @param {boolean?} continueOnReasoning If continuing on reasoning | ||
| 3211 | */ | ||
| 3212 | async #checkDomElements(messageId, continueOnReasoning = null) { | ||
| 3208 | if (this.messageDom === null || this.messageTextDom === null) { | 3213 | if (this.messageDom === null || this.messageTextDom === null) { |
| 3209 | this.messageDom = document.querySelector(`#chat .mes[mesid="${messageId}"]`); | 3214 | this.messageDom = document.querySelector(`#chat .mes[mesid="${messageId}"]`); |
| 3210 | this.messageTextDom = this.messageDom?.querySelector('.mes_text'); | 3215 | this.messageTextDom = this.messageDom?.querySelector('.mes_text'); |
| 3211 | this.messageTimerDom = this.messageDom?.querySelector('.mes_timer'); | 3216 | this.messageTimerDom = this.messageDom?.querySelector('.mes_timer'); |
| 3212 | this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay'); | 3217 | this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay'); |
| 3213 | } | 3218 | } |
| 3219 | if (continueOnReasoning) { | ||
| 3220 | await this.reasoningHandler.process(messageId, false, this.promptReasoning); | ||
| 3221 | } | ||
| 3214 | this.reasoningHandler.updateDom(messageId); | 3222 | this.reasoningHandler.updateDom(messageId); |
| 3215 | } | 3223 | } |
| 3216 | 3224 | ||
| @@ -3230,7 +3238,8 @@ class StreamingProcessor { | |||
| 3230 | } | 3238 | } |
| 3231 | 3239 | ||
| 3232 | async onStartStreaming(text) { | 3240 | async onStartStreaming(text) { |
| 3233 | if (this.type === 'continue' && this.promptReasoning.prefixReasoning) { | 3241 | const continueOnReasoning = !!(this.type === 'continue' && this.promptReasoning.prefixReasoning); |
| 3242 | if (continueOnReasoning) { | ||
| 3234 | this.reasoningHandler.initContinue(this.promptReasoning); | 3243 | this.reasoningHandler.initContinue(this.promptReasoning); |
| 3235 | } | 3244 | } |
| 3236 | 3245 | ||
| @@ -3242,7 +3251,7 @@ class StreamingProcessor { | |||
| 3242 | } else { | 3251 | } else { |
| 3243 | await saveReply(this.type, text, true, '', [], ''); | 3252 | await saveReply(this.type, text, true, '', [], ''); |
| 3244 | messageId = chat.length - 1; | 3253 | messageId = chat.length - 1; |
| 3245 | this.#checkDomElements(messageId); | 3254 | await this.#checkDomElements(messageId, continueOnReasoning); |
| 3246 | this.markUIGenStarted(); | 3255 | this.markUIGenStarted(); |
| 3247 | } | 3256 | } |
| 3248 | hideSwipeButtons(); | 3257 | hideSwipeButtons(); |
| @@ -3275,7 +3284,7 @@ class StreamingProcessor { | |||
| 3275 | this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true })); | 3284 | this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true })); |
| 3276 | } else { | 3285 | } else { |
| 3277 | const mesChanged = chat[messageId]['mes'] !== processedText; | 3286 | const mesChanged = chat[messageId]['mes'] !== processedText; |
| 3278 | this.#checkDomElements(messageId); | 3287 | await this.#checkDomElements(messageId); |
| 3279 | this.#updateMessageBlockVisibility(); | 3288 | this.#updateMessageBlockVisibility(); |
| 3280 | const currentTime = new Date(); | 3289 | const currentTime = new Date(); |
| 3281 | chat[messageId]['mes'] = processedText; | 3290 | chat[messageId]['mes'] = processedText; |
| @@ -3287,7 +3296,7 @@ class StreamingProcessor { | |||
| 3287 | chat[messageId]['extra']['time_to_first_token'] = this.timeToFirstToken; | 3296 | chat[messageId]['extra']['time_to_first_token'] = this.timeToFirstToken; |
| 3288 | 3297 | ||
| 3289 | // Update reasoning | 3298 | // Update reasoning |
| 3290 | await this.reasoningHandler.process(messageId, mesChanged); | 3299 | await this.reasoningHandler.process(messageId, mesChanged, this.promptReasoning); |
| 3291 | processedText = chat[messageId]['mes']; | 3300 | processedText = chat[messageId]['mes']; |
| 3292 | 3301 | ||
| 3293 | // Token count update. | 3302 | // Token count update. |
| @@ -5957,7 +5966,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc | |||
| 5957 | getMessage = trimToEndSentence(getMessage); | 5966 | getMessage = trimToEndSentence(getMessage); |
| 5958 | } | 5967 | } |
| 5959 | 5968 | ||
| 5960 | if (power_user.trim_spaces) { | 5969 | if (power_user.trim_spaces && !PromptReasoning.getLatestPrefix()) { |
| 5961 | getMessage = getMessage.trim(); | 5970 | getMessage = getMessage.trim(); |
| 5962 | } | 5971 | } |
| 5963 | 5972 | ||
| @@ -196,7 +196,7 @@ export class ReasoningHandler { | |||
| 196 | */ | 196 | */ |
| 197 | initContinue(promptReasoning) { | 197 | initContinue(promptReasoning) { |
| 198 | this.reasoning = promptReasoning.prefixReasoning; | 198 | this.reasoning = promptReasoning.prefixReasoning; |
| 199 | this.state = ReasoningState.Done; | 199 | this.state = promptReasoning.prefixIncomplete ? ReasoningState.None : ReasoningState.Done; |
| 200 | this.startTime = this.initialTime; | 200 | this.startTime = this.initialTime; |
| 201 | this.endTime = promptReasoning.prefixDuration ? new Date(this.initialTime.getTime() + promptReasoning.prefixDuration) : null; | 201 | this.endTime = promptReasoning.prefixDuration ? new Date(this.initialTime.getTime() + promptReasoning.prefixDuration) : null; |
| 202 | } | 202 | } |
| @@ -324,10 +324,11 @@ export class ReasoningHandler { | |||
| 324 | * | 324 | * |
| 325 | * @param {number} messageId - The ID of the message to process | 325 | * @param {number} messageId - The ID of the message to process |
| 326 | * @param {boolean} mesChanged - Whether the message has changed | 326 | * @param {boolean} mesChanged - Whether the message has changed |
| 327 | * @param {PromptReasoning} promptReasoning - Prompt reasoning object | ||
| 327 | * @returns {Promise<void>} | 328 | * @returns {Promise<void>} |
| 328 | */ | 329 | */ |
| 329 | async process(messageId, mesChanged) { | 330 | async process(messageId, mesChanged, promptReasoning) { |
| 330 | mesChanged = this.#autoParseReasoningFromMessage(messageId, mesChanged); | 331 | mesChanged = this.#autoParseReasoningFromMessage(messageId, mesChanged, promptReasoning); |
| 331 | 332 | ||
| 332 | if (!this.reasoning && !this.#isHiddenReasoningModel) | 333 | if (!this.reasoning && !this.#isHiddenReasoningModel) |
| 333 | return; | 334 | return; |
| @@ -345,7 +346,14 @@ export class ReasoningHandler { | |||
| 345 | } | 346 | } |
| 346 | } | 347 | } |
| 347 | 348 | ||
| 348 | #autoParseReasoningFromMessage(messageId, mesChanged) { | 349 | /** |
| 350 | * Parse reasoning from a message during streaming. | ||
| 351 | * @param {number} messageId Message ID | ||
| 352 | * @param {boolean} mesChanged Whether the message has changed before reasoning parsing | ||
| 353 | * @param {PromptReasoning} promptReasoning Prompt reasoning object | ||
| 354 | * @returns {boolean} Whether the message has changed after reasoning parsing | ||
| 355 | */ | ||
| 356 | #autoParseReasoningFromMessage(messageId, mesChanged, promptReasoning) { | ||
| 349 | if (!power_user.reasoning.auto_parse) | 357 | if (!power_user.reasoning.auto_parse) |
| 350 | return; | 358 | return; |
| 351 | if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) | 359 | if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) |
| @@ -355,15 +363,17 @@ export class ReasoningHandler { | |||
| 355 | const message = chat[messageId]; | 363 | const message = chat[messageId]; |
| 356 | if (!message) return mesChanged; | 364 | if (!message) return mesChanged; |
| 357 | 365 | ||
| 366 | const parseTarget = promptReasoning?.prefixIncomplete ? (promptReasoning.prefixReasoningFormatted + message.mes) : message.mes; | ||
| 367 | |||
| 358 | // If we are done with reasoning parse, we just split the message correctly so the reasoning doesn't show up inside of it. | 368 | // If we are done with reasoning parse, we just split the message correctly so the reasoning doesn't show up inside of it. |
| 359 | if (this.#parsingReasoningMesStartIndex) { | 369 | if (this.#parsingReasoningMesStartIndex) { |
| 360 | message.mes = trimSpaces(message.mes.slice(this.#parsingReasoningMesStartIndex)); | 370 | message.mes = trimSpaces(parseTarget.slice(this.#parsingReasoningMesStartIndex)); |
| 361 | return mesChanged; | 371 | return mesChanged; |
| 362 | } | 372 | } |
| 363 | 373 | ||
| 364 | if (this.state === ReasoningState.None || this.#isHiddenReasoningModel) { | 374 | if (this.state === ReasoningState.None || this.#isHiddenReasoningModel) { |
| 365 | // If streamed message starts with the opening, cut it out and put all inside reasoning | 375 | // If streamed message starts with the opening, cut it out and put all inside reasoning |
| 366 | if (message.mes.startsWith(power_user.reasoning.prefix) && message.mes.length > power_user.reasoning.prefix.length) { | 376 | if (parseTarget.startsWith(power_user.reasoning.prefix) && parseTarget.length > power_user.reasoning.prefix.length) { |
| 367 | this.#isParsingReasoning = true; | 377 | this.#isParsingReasoning = true; |
| 368 | 378 | ||
| 369 | // Manually set starting state here, as we might already have received the ending suffix | 379 | // Manually set starting state here, as we might already have received the ending suffix |
| @@ -377,15 +387,14 @@ export class ReasoningHandler { | |||
| 377 | return mesChanged; | 387 | return mesChanged; |
| 378 | 388 | ||
| 379 | // If we are in manual parsing mode, all currently streaming mes tokens will go the the reasoning block | 389 | // If we are in manual parsing mode, all currently streaming mes tokens will go the the reasoning block |
| 380 | const originalMes = message.mes; | 390 | this.reasoning = parseTarget.slice(power_user.reasoning.prefix.length); |
| 381 | this.reasoning = originalMes.slice(power_user.reasoning.prefix.length); | ||
| 382 | message.mes = ''; | 391 | message.mes = ''; |
| 383 | 392 | ||
| 384 | // If the reasoning contains the ending suffix, we cut that off and continue as message streaming | 393 | // If the reasoning contains the ending suffix, we cut that off and continue as message streaming |
| 385 | if (this.reasoning.includes(power_user.reasoning.suffix)) { | 394 | if (this.reasoning.includes(power_user.reasoning.suffix)) { |
| 386 | this.reasoning = this.reasoning.slice(0, this.reasoning.indexOf(power_user.reasoning.suffix)); | 395 | this.reasoning = this.reasoning.slice(0, this.reasoning.indexOf(power_user.reasoning.suffix)); |
| 387 | this.#parsingReasoningMesStartIndex = originalMes.indexOf(power_user.reasoning.suffix) + power_user.reasoning.suffix.length; | 396 | this.#parsingReasoningMesStartIndex = parseTarget.indexOf(power_user.reasoning.suffix) + power_user.reasoning.suffix.length; |
| 388 | message.mes = trimSpaces(originalMes.slice(this.#parsingReasoningMesStartIndex)); | 397 | message.mes = trimSpaces(parseTarget.slice(this.#parsingReasoningMesStartIndex)); |
| 389 | this.#isParsingReasoning = false; | 398 | this.#isParsingReasoning = false; |
| 390 | } | 399 | } |
| 391 | 400 | ||
| @@ -525,13 +534,56 @@ export class ReasoningHandler { | |||
| 525 | * Keeps track of the number of reasoning additions. | 534 | * Keeps track of the number of reasoning additions. |
| 526 | */ | 535 | */ |
| 527 | export class PromptReasoning { | 536 | export class PromptReasoning { |
| 537 | /** | ||
| 538 | * An instance initiated during the latest prompt processing. | ||
| 539 | * @type {PromptReasoning} | ||
| 540 | * */ | ||
| 541 | static #LATEST = null; | ||
| 542 | /** | ||
| 543 | * @readonly Zero-width space character used as a placeholder for reasoning. | ||
| 544 | * @type {string} | ||
| 545 | */ | ||
| 528 | static REASONING_PLACEHOLDER = '\u200B'; | 546 | static REASONING_PLACEHOLDER = '\u200B'; |
| 529 | 547 | ||
| 548 | /** | ||
| 549 | * Returns the latest formatted reasoning prefix if the prefix is incomplete. | ||
| 550 | * @returns {string} Formatted reasoning prefix | ||
| 551 | */ | ||
| 552 | static getLatestPrefix() { | ||
| 553 | if (!PromptReasoning.#LATEST) { | ||
| 554 | return ''; | ||
| 555 | } | ||
| 556 | |||
| 557 | if (!PromptReasoning.#LATEST.prefixIncomplete) { | ||
| 558 | return ''; | ||
| 559 | } | ||
| 560 | |||
| 561 | return PromptReasoning.#LATEST.prefixReasoningFormatted; | ||
| 562 | } | ||
| 563 | |||
| 564 | /** | ||
| 565 | * Free the latest reasoning instance. | ||
| 566 | * To be called when the generation has ended or stopped. | ||
| 567 | */ | ||
| 568 | static clearLatest() { | ||
| 569 | PromptReasoning.#LATEST = null; | ||
| 570 | } | ||
| 571 | |||
| 530 | constructor() { | 572 | constructor() { |
| 573 | PromptReasoning.#LATEST = this; | ||
| 574 | |||
| 575 | /** @type {number} */ | ||
| 531 | this.counter = 0; | 576 | this.counter = 0; |
| 577 | /** @type {number} */ | ||
| 532 | this.prefixLength = -1; | 578 | this.prefixLength = -1; |
| 579 | /** @type {string} */ | ||
| 533 | this.prefixReasoning = ''; | 580 | this.prefixReasoning = ''; |
| 581 | /** @type {string} */ | ||
| 582 | this.prefixReasoningFormatted = ''; | ||
| 583 | /** @type {number?} */ | ||
| 534 | this.prefixDuration = null; | 584 | this.prefixDuration = null; |
| 585 | /** @type {boolean} */ | ||
| 586 | this.prefixIncomplete = false; | ||
| 535 | } | 587 | } |
| 536 | 588 | ||
| 537 | /** | 589 | /** |
| @@ -578,8 +630,10 @@ export class PromptReasoning { | |||
| 578 | const formattedReasoning = `${prefix}${reasoning}`; | 630 | const formattedReasoning = `${prefix}${reasoning}`; |
| 579 | if (isPrefix) { | 631 | if (isPrefix) { |
| 580 | this.prefixReasoning = reasoning; | 632 | this.prefixReasoning = reasoning; |
| 633 | this.prefixReasoningFormatted = formattedReasoning; | ||
| 581 | this.prefixLength = formattedReasoning.length; | 634 | this.prefixLength = formattedReasoning.length; |
| 582 | this.prefixDuration = duration; | 635 | this.prefixDuration = duration; |
| 636 | this.prefixIncomplete = true; | ||
| 583 | } | 637 | } |
| 584 | return formattedReasoning; | 638 | return formattedReasoning; |
| 585 | } | 639 | } |
| @@ -588,8 +642,10 @@ export class PromptReasoning { | |||
| 588 | const formattedReasoning = `${prefix}${reasoning}${suffix}${separator}`; | 642 | const formattedReasoning = `${prefix}${reasoning}${suffix}${separator}`; |
| 589 | if (isPrefix) { | 643 | if (isPrefix) { |
| 590 | this.prefixReasoning = reasoning; | 644 | this.prefixReasoning = reasoning; |
| 645 | this.prefixReasoningFormatted = formattedReasoning; | ||
| 591 | this.prefixLength = formattedReasoning.length; | 646 | this.prefixLength = formattedReasoning.length; |
| 592 | this.prefixDuration = duration; | 647 | this.prefixDuration = duration; |
| 648 | this.prefixIncomplete = false; | ||
| 593 | } | 649 | } |
| 594 | return `${formattedReasoning}${content}`; | 650 | return `${formattedReasoning}${content}`; |
| 595 | } | 651 | } |
| @@ -1075,12 +1131,13 @@ export function parseReasoningInSwipes(swipes, swipeInfoArray, duration) { | |||
| 1075 | } | 1131 | } |
| 1076 | 1132 | ||
| 1077 | function registerReasoningAppEvents() { | 1133 | function registerReasoningAppEvents() { |
| 1078 | const eventHandler = (/** @type {number} */ idx) => { | 1134 | const eventHandler = (/** @type {string} */ type, /** @type {number} */ idx) => { |
| 1079 | if (!power_user.reasoning.auto_parse) { | 1135 | if (!power_user.reasoning.auto_parse) { |
| 1080 | return; | 1136 | return; |
| 1081 | } | 1137 | } |
| 1082 | 1138 | ||
| 1083 | console.debug('[Reasoning] Auto-parsing reasoning block for message', idx); | 1139 | console.debug('[Reasoning] Auto-parsing reasoning block for message', idx); |
| 1140 | const prefix = type === event_types.MESSAGE_RECEIVED ? PromptReasoning.getLatestPrefix() : ''; | ||
| 1084 | const message = chat[idx]; | 1141 | const message = chat[idx]; |
| 1085 | 1142 | ||
| 1086 | if (!message) { | 1143 | if (!message) { |
| @@ -1093,12 +1150,12 @@ function registerReasoningAppEvents() { | |||
| 1093 | return null; | 1150 | return null; |
| 1094 | } | 1151 | } |
| 1095 | 1152 | ||
| 1096 | if (message.extra?.reasoning) { | 1153 | if (message.extra?.reasoning && !prefix) { |
| 1097 | console.debug('[Reasoning] Message already has reasoning', idx); | 1154 | console.debug('[Reasoning] Message already has reasoning', idx); |
| 1098 | return null; | 1155 | return null; |
| 1099 | } | 1156 | } |
| 1100 | 1157 | ||
| 1101 | const parsedReasoning = parseReasoningFromString(message.mes); | 1158 | const parsedReasoning = parseReasoningFromString(prefix + message.mes); |
| 1102 | 1159 | ||
| 1103 | // No reasoning block found | 1160 | // No reasoning block found |
| 1104 | if (!parsedReasoning) { | 1161 | if (!parsedReasoning) { |
| @@ -1137,7 +1194,11 @@ function registerReasoningAppEvents() { | |||
| 1137 | }; | 1194 | }; |
| 1138 | 1195 | ||
| 1139 | for (const event of [event_types.MESSAGE_RECEIVED, event_types.MESSAGE_UPDATED]) { | 1196 | for (const event of [event_types.MESSAGE_RECEIVED, event_types.MESSAGE_UPDATED]) { |
| 1140 | eventSource.on(event, eventHandler); | 1197 | eventSource.on(event, (/** @type {number} */ idx) => eventHandler(event, idx)); |
| 1198 | } | ||
| 1199 | |||
| 1200 | for (const event of [event_types.GENERATION_STOPPED, event_types.GENERATION_ENDED, event_types.CHAT_CHANGED]) { | ||
| 1201 | eventSource.on(event, () => PromptReasoning.clearLatest()); | ||
| 1141 | } | 1202 | } |
| 1142 | } | 1203 | } |
| 1143 | 1204 | ||