Merge pull request #3634 from SillyTavern/continue-from-reasoning Fix auto-parsing of continue from reasoning
Signed| @@ -3204,13 +3204,21 @@ class StreamingProcessor { | ||
| 3204 | 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 | 3213 | if (this.messageDom === null || this.messageTextDom === null) { |
| 3209 | 3214 | this.messageDom = document.querySelector(`#chat .mes[mesid="${messageId}"]`); |
| 3210 | 3215 | this.messageTextDom = this.messageDom?.querySelector('.mes_text'); |
| 3211 | 3216 | this.messageTimerDom = this.messageDom?.querySelector('.mes_timer'); |
| 3212 | 3217 | this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay'); |
| 3213 | 3218 | } |
| 3219 | + if (continueOnReasoning) { | |
| 3220 | + await this.reasoningHandler.process(messageId, false, this.promptReasoning); | |
| 3221 | + } | |
| 3214 | 3222 | this.reasoningHandler.updateDom(messageId); |
| 3215 | 3223 | } |
| 3216 | 3224 | |
| @@ -3230,7 +3238,8 @@ class StreamingProcessor { | ||
| 3230 | 3238 | } |
| 3231 | 3239 | |
| 3232 | 3240 | async onStartStreaming(text) { |
| 3233 | 3241 | ifconst continueOnReasoning = !!(this.type === 'continue' && this.promptReasoning.prefixReasoning) {; |
| 3242 | + if (continueOnReasoning) { | |
| 3234 | 3243 | this.reasoningHandler.initContinue(this.promptReasoning); |
| 3235 | 3244 | } |
| 3236 | 3245 | |
| @@ -3242,7 +3251,7 @@ class StreamingProcessor { | ||
| 3242 | 3251 | } else { |
| 3243 | 3252 | await saveReply(this.type, text, true, '', [], ''); |
| 3244 | 3253 | messageId = chat.length - 1; |
| 3245 | 3254 | await this.#checkDomElements(messageId, continueOnReasoning); |
| 3246 | 3255 | this.markUIGenStarted(); |
| 3247 | 3256 | } |
| 3248 | 3257 | hideSwipeButtons(); |
| @@ -3275,7 +3284,7 @@ class StreamingProcessor { | ||
| 3275 | 3284 | this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true })); |
| 3276 | 3285 | } else { |
| 3277 | 3286 | const mesChanged = chat[messageId]['mes'] !== processedText; |
| 3278 | 3287 | await this.#checkDomElements(messageId); |
| 3279 | 3288 | this.#updateMessageBlockVisibility(); |
| 3280 | 3289 | const currentTime = new Date(); |
| 3281 | 3290 | chat[messageId]['mes'] = processedText; |
| @@ -3287,7 +3296,7 @@ class StreamingProcessor { | ||
| 3287 | 3296 | chat[messageId]['extra']['time_to_first_token'] = this.timeToFirstToken; |
| 3288 | 3297 | |
| 3289 | 3298 | // Update reasoning |
| 3290 | 3299 | await this.reasoningHandler.process(messageId, mesChanged, this.promptReasoning); |
| 3291 | 3300 | processedText = chat[messageId]['mes']; |
| 3292 | 3301 | |
| 3293 | 3302 | // Token count update. |
| @@ -5957,7 +5966,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc | ||
| 5957 | 5966 | getMessage = trimToEndSentence(getMessage); |
| 5958 | 5967 | } |
| 5959 | 5968 | |
| 5960 | 5969 | if (power_user.trim_spaces && !PromptReasoning.getLatestPrefix()) { |
| 5961 | 5970 | getMessage = getMessage.trim(); |
| 5962 | 5971 | } |
| 5963 | 5972 | |
| @@ -196,7 +196,7 @@ export class ReasoningHandler { | ||
| 196 | 196 | */ |
| 197 | 197 | initContinue(promptReasoning) { |
| 198 | 198 | this.reasoning = promptReasoning.prefixReasoning; |
| 199 | 199 | this.state = promptReasoning.prefixIncomplete ? ReasoningState.None : ReasoningState.Done; |
| 200 | 200 | this.startTime = this.initialTime; |
| 201 | 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 | 325 | * @param {number} messageId - The ID of the message to process |
| 326 | 326 | * @param {boolean} mesChanged - Whether the message has changed |
| 327 | + * @param {PromptReasoning} promptReasoning - Prompt reasoning object | |
| 327 | 328 | * @returns {Promise<void>} |
| 328 | 329 | */ |
| 329 | 330 | async process(messageId, mesChanged, promptReasoning) { |
| 330 | 331 | mesChanged = this.#autoParseReasoningFromMessage(messageId, mesChanged, promptReasoning); |
| 331 | 332 | |
| 332 | 333 | if (!this.reasoning && !this.#isHiddenReasoningModel) |
| 333 | 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 | 357 | if (!power_user.reasoning.auto_parse) |
| 350 | 358 | return; |
| 351 | 359 | if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) |
| @@ -355,15 +363,17 @@ export class ReasoningHandler { | ||
| 355 | 363 | const message = chat[messageId]; |
| 356 | 364 | if (!message) return mesChanged; |
| 357 | 365 | |
| 366 | + const parseTarget = promptReasoning?.prefixIncomplete ? (promptReasoning.prefixReasoningFormatted + message.mes) : message.mes; | |
| 367 | + | |
| 358 | 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 | 369 | if (this.#parsingReasoningMesStartIndex) { |
| 360 | 370 | message.mes = trimSpaces(message.mesparseTarget.slice(this.#parsingReasoningMesStartIndex)); |
| 361 | 371 | return mesChanged; |
| 362 | 372 | } |
| 363 | 373 | |
| 364 | 374 | if (this.state === ReasoningState.None || this.#isHiddenReasoningModel) { |
| 365 | 375 | // If streamed message starts with the opening, cut it out and put all inside reasoning |
| 366 | 376 | if (message.mesparseTarget.startsWith(power_user.reasoning.prefix) && message.mesparseTarget.length > power_user.reasoning.prefix.length) { |
| 367 | 377 | this.#isParsingReasoning = true; |
| 368 | 378 | |
| 369 | 379 | // Manually set starting state here, as we might already have received the ending suffix |
| @@ -377,15 +387,14 @@ export class ReasoningHandler { | ||
| 377 | 387 | return mesChanged; |
| 378 | 388 | |
| 379 | 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 | 391 | message.mes = ''; |
| 383 | 392 | |
| 384 | 393 | // If the reasoning contains the ending suffix, we cut that off and continue as message streaming |
| 385 | 394 | if (this.reasoning.includes(power_user.reasoning.suffix)) { |
| 386 | 395 | this.reasoning = this.reasoning.slice(0, this.reasoning.indexOf(power_user.reasoning.suffix)); |
| 387 | 396 | this.#parsingReasoningMesStartIndex = originalMesparseTarget.indexOf(power_user.reasoning.suffix) + power_user.reasoning.suffix.length; |
| 388 | 397 | message.mes = trimSpaces(originalMesparseTarget.slice(this.#parsingReasoningMesStartIndex)); |
| 389 | 398 | this.#isParsingReasoning = false; |
| 390 | 399 | } |
| 391 | 400 | |
| @@ -525,13 +534,56 @@ export class ReasoningHandler { | ||
| 525 | 534 | * Keeps track of the number of reasoning additions. |
| 526 | 535 | */ |
| 527 | 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 | 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 | 572 | constructor() { |
| 573 | + PromptReasoning.#LATEST = this; | |
| 574 | + | |
| 575 | + /** @type {number} */ | |
| 531 | 576 | this.counter = 0; |
| 577 | + /** @type {number} */ | |
| 532 | 578 | this.prefixLength = -1; |
| 579 | + /** @type {string} */ | |
| 533 | 580 | this.prefixReasoning = ''; |
| 581 | + /** @type {string} */ | |
| 582 | + this.prefixReasoningFormatted = ''; | |
| 583 | + /** @type {number?} */ | |
| 534 | 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 | 630 | const formattedReasoning = `${prefix}${reasoning}`; |
| 579 | 631 | if (isPrefix) { |
| 580 | 632 | this.prefixReasoning = reasoning; |
| 633 | + this.prefixReasoningFormatted = formattedReasoning; | |
| 581 | 634 | this.prefixLength = formattedReasoning.length; |
| 582 | 635 | this.prefixDuration = duration; |
| 636 | + this.prefixIncomplete = true; | |
| 583 | 637 | } |
| 584 | 638 | return formattedReasoning; |
| 585 | 639 | } |
| @@ -588,8 +642,10 @@ export class PromptReasoning { | ||
| 588 | 642 | const formattedReasoning = `${prefix}${reasoning}${suffix}${separator}`; |
| 589 | 643 | if (isPrefix) { |
| 590 | 644 | this.prefixReasoning = reasoning; |
| 645 | + this.prefixReasoningFormatted = formattedReasoning; | |
| 591 | 646 | this.prefixLength = formattedReasoning.length; |
| 592 | 647 | this.prefixDuration = duration; |
| 648 | + this.prefixIncomplete = false; | |
| 593 | 649 | } |
| 594 | 650 | return `${formattedReasoning}${content}`; |
| 595 | 651 | } |
| @@ -1075,12 +1131,13 @@ export function parseReasoningInSwipes(swipes, swipeInfoArray, duration) { | ||
| 1075 | 1131 | } |
| 1076 | 1132 | |
| 1077 | 1133 | function registerReasoningAppEvents() { |
| 1078 | 1134 | const eventHandler = (/** @type {string} */ type, /** @type {number} */ idx) => { |
| 1079 | 1135 | if (!power_user.reasoning.auto_parse) { |
| 1080 | 1136 | return; |
| 1081 | 1137 | } |
| 1082 | 1138 | |
| 1083 | 1139 | console.debug('[Reasoning] Auto-parsing reasoning block for message', idx); |
| 1140 | + const prefix = type === event_types.MESSAGE_RECEIVED ? PromptReasoning.getLatestPrefix() : ''; | |
| 1084 | 1141 | const message = chat[idx]; |
| 1085 | 1142 | |
| 1086 | 1143 | if (!message) { |
| @@ -1093,12 +1150,12 @@ function registerReasoningAppEvents() { | ||
| 1093 | 1150 | return null; |
| 1094 | 1151 | } |
| 1095 | 1152 | |
| 1096 | 1153 | if (message.extra?.reasoning && !prefix) { |
| 1097 | 1154 | console.debug('[Reasoning] Message already has reasoning', idx); |
| 1098 | 1155 | return null; |
| 1099 | 1156 | } |
| 1100 | 1157 | |
| 1101 | 1158 | const parsedReasoning = parseReasoningFromString(prefix + message.mes); |
| 1102 | 1159 | |
| 1103 | 1160 | // No reasoning block found |
| 1104 | 1161 | if (!parsedReasoning) { |
| @@ -1137,7 +1194,11 @@ function registerReasoningAppEvents() { | ||
| 1137 | 1194 | }; |
| 1138 | 1195 | |
| 1139 | 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 | |