Merge pull request #3634 from SillyTavern/continue-from-reasoning Fix auto-parsing of continue from reasoning

a392593e534551998116c2faf4da349088d02f88

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
2 files changed, +90 -20Ignore whitespace
public/script.js+15 -6
@@ -3204,13 +3204,21 @@ class StreamingProcessor {
3204 this.promptReasoning = promptReasoning;3204 this.promptReasoning = promptReasoning;
3205 }3205 }
32063206
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 }
32163224
@@ -3230,7 +3238,8 @@ class StreamingProcessor {
3230 }3238 }
32313239
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 }
32363245
@@ -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;
32883297
3289 // Update reasoning3298 // 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'];
32923301
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 }
59595968
5960 if (power_user.trim_spaces) {5969 if (power_user.trim_spaces && !PromptReasoning.getLatestPrefix()) {
5961 getMessage = getMessage.trim();5970 getMessage = getMessage.trim();
5962 }5971 }
59635972
public/scripts/reasoning.js+75 -14
@@ -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 process325 * @param {number} messageId - The ID of the message to process
326 * @param {boolean} mesChanged - Whether the message has changed326 * @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);
331332
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 }
347348
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;
357365
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 }
363373
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 reasoning375 // 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;
368378
369 // Manually set starting state here, as we might already have received the ending suffix379 // 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;
378388
379 // If we are in manual parsing mode, all currently streaming mes tokens will go the the reasoning block389 // 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 = '';
383392
384 // If the reasoning contains the ending suffix, we cut that off and continue as message streaming393 // 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 }
391400
@@ -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 */
527export class PromptReasoning {536export 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';
529547
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 }
536588
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}
10761132
1077function registerReasoningAppEvents() {1133function 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 }
10821138
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];
10851142
1086 if (!message) {1143 if (!message) {
@@ -1093,12 +1150,12 @@ function registerReasoningAppEvents() {
1093 return null;1150 return null;
1094 }1151 }
10951152
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 }
11001157
1101 const parsedReasoning = parseReasoningFromString(message.mes);1158 const parsedReasoning = parseReasoningFromString(prefix + message.mes);
11021159
1103 // No reasoning block found1160 // No reasoning block found
1104 if (!parsedReasoning) {1161 if (!parsedReasoning) {
@@ -1137,7 +1194,11 @@ function registerReasoningAppEvents() {
1137 };1194 };
11381195
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}
11431204