Summarize: dedicated LLM connection profile for summarization Reviewed-by: auto-review
| @@ -27,7 +27,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; | ||
| 27 | 27 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
| 28 | 28 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; |
| 29 | 29 | import { macros, MacroCategory } from '../../macros/macro-system.js'; |
| 30 | 30 | import { ConnectionManagerRequestService, countWebLlmTokens, generateWebLlmChatPrompt, getWebLlmContextSize, isWebLlmSupported } from '../shared.js'; |
| 31 | 31 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 32 | 32 | import { removeReasoningFromString } from '../../reasoning.js'; |
| 33 | 33 | import { MacrosParser } from '/scripts/macros.js'; |
| @@ -137,6 +137,7 @@ const defaultSettings = { | ||
| 137 | 137 | maxMessagesPerRequestStep: 1, |
| 138 | 138 | prompt_builder: prompt_builders.DEFAULT, |
| 139 | 139 | summaryPromptRole: extension_prompt_roles.SYSTEM, |
| 140 | + summaryConnectionProfile: '', | |
| 140 | 141 | }; |
| 141 | 142 | |
| 142 | 143 | /** |
| @@ -150,6 +151,34 @@ function getSummaryPromptRole() { | ||
| 150 | 151 | : extension_prompt_roles.SYSTEM; |
| 151 | 152 | } |
| 152 | 153 | |
| 154 | +let summaryConnectionProfileDropdownInitialized = false; | |
| 155 | + | |
| 156 | +/** | |
| 157 | + * Populates the dedicated summarization connection profile dropdown. | |
| 158 | + * Only initializes once to avoid attaching duplicate Connection Manager event listeners | |
| 159 | + * when loadSettings() is called again (e.g. after loading a settings preset). | |
| 160 | + */ | |
| 161 | +function initSummaryConnectionProfileDropdown() { | |
| 162 | + if (summaryConnectionProfileDropdownInitialized) { | |
| 163 | + return; | |
| 164 | + } | |
| 165 | + | |
| 166 | + try { | |
| 167 | + ConnectionManagerRequestService.handleDropdown( | |
| 168 | + '#memory_summary_connection_profile', | |
| 169 | + extension_settings.memory.summaryConnectionProfile, | |
| 170 | + (profile) => { | |
| 171 | + extension_settings.memory.summaryConnectionProfile = profile?.id ?? ''; | |
| 172 | + saveSettingsDebounced(); | |
| 173 | + }, | |
| 174 | + ); | |
| 175 | + summaryConnectionProfileDropdownInitialized = true; | |
| 176 | + } catch (error) { | |
| 177 | + // Connection Manager may be unavailable/disabled; leave the dropdown empty in that case. | |
| 178 | + console.warn('Summarize: could not populate summary connection profile dropdown', error); | |
| 179 | + } | |
| 180 | +} | |
| 181 | + | |
| 153 | 182 | function loadSettings() { |
| 154 | 183 | if (Object.keys(extension_settings.memory).length === 0) { |
| 155 | 184 | Object.assign(extension_settings.memory, defaultSettings); |
| @@ -171,6 +200,7 @@ function loadSettings() { | ||
| 171 | 200 | $('#memory_depth').val(extension_settings.memory.depth).trigger('input'); |
| 172 | 201 | $('#memory_role').val(extension_settings.memory.role).trigger('input'); |
| 173 | 202 | $('#memory_summary_prompt_role').val(extension_settings.memory.summaryPromptRole).trigger('input'); |
| 203 | + initSummaryConnectionProfileDropdown(); | |
| 174 | 204 | $(`input[name="memory_position"][value="${extension_settings.memory.position}"]`).prop('checked', true).trigger('input'); |
| 175 | 205 | $('#memory_prompt_words_force').val(extension_settings.memory.promptForceWords).trigger('input'); |
| 176 | 206 | $(`input[name="memory_prompt_builder"][value="${extension_settings.memory.prompt_builder}"]`).prop('checked', true).trigger('input'); |
| @@ -705,6 +735,50 @@ async function summarizeChatWebLLM(context, force) { | ||
| 705 | 735 | } |
| 706 | 736 | } |
| 707 | 737 | |
| 738 | +/** | |
| 739 | + * Resolve the maximum response length (in tokens) to request from a connection profile. | |
| 740 | + * Uses the configured override when it is a positive number; otherwise falls back to a | |
| 741 | + * sensible default cap for a summary. | |
| 742 | + * @returns {number} Maximum tokens for the summary response | |
| 743 | + */ | |
| 744 | +function getSummaryProfileMaxTokens() { | |
| 745 | + const override = Number(extension_settings.memory.overrideResponseLength); | |
| 746 | + return override > 0 ? override : 1024; | |
| 747 | +} | |
| 748 | + | |
| 749 | +/** | |
| 750 | + * Generate a summary using a dedicated Connection Manager profile instead of the active model. | |
| 751 | + * Assembles the chat transcript (via getRawSummaryPrompt) together with the summary instruction | |
| 752 | + * so the chosen model actually receives the chat to summarize. Honors the configured summary | |
| 753 | + * prompt role by delivering the instruction as a SYSTEM or USER message. | |
| 754 | + * @param {object} context ST context | |
| 755 | + * @param {string} profileId Connection Manager profile id | |
| 756 | + * @param {string} prompt Summary instruction | |
| 757 | + * @returns {Promise<{summary: string, index: number}>} Generated summary and the index it covers up to | |
| 758 | + */ | |
| 759 | +async function summarizeWithConnectionProfile(context, profileId, prompt) { | |
| 760 | + const { rawPrompt, lastUsedIndex } = await getRawSummaryPrompt(context, prompt); | |
| 761 | + | |
| 762 | + if (lastUsedIndex === null || lastUsedIndex === -1) { | |
| 763 | + // Nothing to summarize; mirror the raw builder's guard. | |
| 764 | + return { summary: '', index: lastUsedIndex }; | |
| 765 | + } | |
| 766 | + | |
| 767 | + const useUserRole = getSummaryPromptRole() === extension_prompt_roles.USER; | |
| 768 | + // sendRequest accepts an array of chat-completion messages, so deliver the instruction | |
| 769 | + // as a separate SYSTEM or USER message (per the configured role) with the transcript as | |
| 770 | + // a USER message. For text-completion profiles these are concatenated downstream. | |
| 771 | + const messages = useUserRole | |
| 772 | + ? [{ role: 'user', content: [prompt, rawPrompt].filter(x => x).join('\n\n') }] | |
| 773 | + : [{ role: 'system', content: prompt }, { role: 'user', content: rawPrompt }]; | |
| 774 | + const filteredMessages = messages.filter(m => m.content); | |
| 775 | + | |
| 776 | + const data = await ConnectionManagerRequestService.sendRequest(profileId, filteredMessages, getSummaryProfileMaxTokens()); | |
| 777 | + // Non-streaming requests resolve to ExtractedData ({ content, reasoning }); extract the plain text. | |
| 778 | + const text = typeof data === 'object' && data !== null ? String(data.content ?? '') : ''; | |
| 779 | + return { summary: removeReasoningFromString(text), index: lastUsedIndex }; | |
| 780 | +} | |
| 781 | + | |
| 708 | 782 | async function summarizeChatMain(context, force, skipWIAN) { |
| 709 | 783 | const prompt = await getSummaryPromptForNow(context, force); |
| 710 | 784 | |
| @@ -716,6 +790,48 @@ async function summarizeChatMain(context, force, skipWIAN) { | ||
| 716 | 790 | let summary = ''; |
| 717 | 791 | let index = null; |
| 718 | 792 | |
| 793 | + // A dedicated connection profile takes precedence over the DEFAULT/RAW builder distinction: | |
| 794 | + // it always assembles the transcript + instruction and routes through Connection Manager. | |
| 795 | + const summaryConnectionProfile = extension_settings.memory.summaryConnectionProfile; | |
| 796 | + if (summaryConnectionProfile) { | |
| 797 | + let profileSucceeded = false; | |
| 798 | + try { | |
| 799 | + inApiCall = true; | |
| 800 | + const result = await summarizeWithConnectionProfile(context, summaryConnectionProfile, prompt); | |
| 801 | + | |
| 802 | + if (result.index === null || result.index === -1) { | |
| 803 | + if (force) { | |
| 804 | + toastr.info('To try again, remove the latest summary.', 'No messages found to summarize'); | |
| 805 | + } | |
| 806 | + return null; | |
| 807 | + } | |
| 808 | + | |
| 809 | + summary = result.summary; | |
| 810 | + index = result.index; | |
| 811 | + profileSucceeded = true; | |
| 812 | + } catch (error) { | |
| 813 | + // Connection Manager disabled, unsupported API, deleted profile, etc. | |
| 814 | + // Fall back to the regular (active model) path below so summarization still works. | |
| 815 | + console.warn('Summarize: connection profile failed, falling back to active model', error); | |
| 816 | + } finally { | |
| 817 | + inApiCall = false; | |
| 818 | + } | |
| 819 | + | |
| 820 | + if (profileSucceeded) { | |
| 821 | + if (!summary) { | |
| 822 | + console.warn('Empty summary received'); | |
| 823 | + return; | |
| 824 | + } | |
| 825 | + | |
| 826 | + if (isContextChanged(context)) { | |
| 827 | + return; | |
| 828 | + } | |
| 829 | + | |
| 830 | + setMemoryContext(summary, true, index); | |
| 831 | + return summary; | |
| 832 | + } | |
| 833 | + } | |
| 834 | + | |
| 719 | 835 | if (prompt_builders.DEFAULT === extension_settings.memory.prompt_builder) { |
| 720 | 836 | try { |
| 721 | 837 | inApiCall = true; |
| @@ -75,6 +75,9 @@ | ||
| 75 | 75 | <option value="1" data-i18n="User">User</option> |
| 76 | 76 | </select> |
| 77 | 77 | <small data-i18n="ext_sum_prompt_role_desc">The role used to send the summary request instruction to the model.</small> |
| 78 | + <label for="memory_summary_connection_profile" data-i18n="ext_sum_connection_profile">Summary Connection Profile</label> | |
| 79 | + <select id="memory_summary_connection_profile" class="text_pole"></select> | |
| 80 | + <small data-i18n="ext_sum_connection_profile_desc">Summarize with a specific connection profile instead of the active model. Leave empty to use the active model.</small> | |
| 78 | 81 | <label for="memory_prompt_words"><span data-i18n="ext_sum_target_length_1">Target summary length</span> <span data-i18n="ext_sum_target_length_2">(</span><span id="memory_prompt_words_value"></span><span data-i18n="ext_sum_target_length_3"> words)</span></label> |
| 79 | 82 | <input id="memory_prompt_words" type="range" value="{{defaultSettings.promptWords}}" min="{{defaultSettings.promptMinWords}}" max="{{defaultSettings.promptMaxWords}}" step="{{defaultSettings.promptWordsStep}}" /> |
| 80 | 83 | <label for="memory_override_response_length"> |