Summarize: dedicated LLM connection profile for summarization Reviewed-by: auto-review
| @@ -27,7 +27,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; | |||
| 27 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; | 27 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
| 28 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; | 28 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; |
| 29 | import { macros, MacroCategory } from '../../macros/macro-system.js'; | 29 | import { macros, MacroCategory } from '../../macros/macro-system.js'; |
| 30 | import { countWebLlmTokens, generateWebLlmChatPrompt, getWebLlmContextSize, isWebLlmSupported } from '../shared.js'; | 30 | import { ConnectionManagerRequestService, countWebLlmTokens, generateWebLlmChatPrompt, getWebLlmContextSize, isWebLlmSupported } from '../shared.js'; |
| 31 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; | 31 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 32 | import { removeReasoningFromString } from '../../reasoning.js'; | 32 | import { removeReasoningFromString } from '../../reasoning.js'; |
| 33 | import { MacrosParser } from '/scripts/macros.js'; | 33 | import { MacrosParser } from '/scripts/macros.js'; |
| @@ -137,6 +137,7 @@ const defaultSettings = { | |||
| 137 | maxMessagesPerRequestStep: 1, | 137 | maxMessagesPerRequestStep: 1, |
| 138 | prompt_builder: prompt_builders.DEFAULT, | 138 | prompt_builder: prompt_builders.DEFAULT, |
| 139 | summaryPromptRole: extension_prompt_roles.SYSTEM, | 139 | summaryPromptRole: extension_prompt_roles.SYSTEM, |
| 140 | summaryConnectionProfile: '', | ||
| 140 | }; | 141 | }; |
| 141 | 142 | ||
| 142 | /** | 143 | /** |
| @@ -150,6 +151,34 @@ function getSummaryPromptRole() { | |||
| 150 | : extension_prompt_roles.SYSTEM; | 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 | function loadSettings() { | 182 | function loadSettings() { |
| 154 | if (Object.keys(extension_settings.memory).length === 0) { | 183 | if (Object.keys(extension_settings.memory).length === 0) { |
| 155 | Object.assign(extension_settings.memory, defaultSettings); | 184 | Object.assign(extension_settings.memory, defaultSettings); |
| @@ -171,6 +200,7 @@ function loadSettings() { | |||
| 171 | $('#memory_depth').val(extension_settings.memory.depth).trigger('input'); | 200 | $('#memory_depth').val(extension_settings.memory.depth).trigger('input'); |
| 172 | $('#memory_role').val(extension_settings.memory.role).trigger('input'); | 201 | $('#memory_role').val(extension_settings.memory.role).trigger('input'); |
| 173 | $('#memory_summary_prompt_role').val(extension_settings.memory.summaryPromptRole).trigger('input'); | 202 | $('#memory_summary_prompt_role').val(extension_settings.memory.summaryPromptRole).trigger('input'); |
| 203 | initSummaryConnectionProfileDropdown(); | ||
| 174 | $(`input[name="memory_position"][value="${extension_settings.memory.position}"]`).prop('checked', true).trigger('input'); | 204 | $(`input[name="memory_position"][value="${extension_settings.memory.position}"]`).prop('checked', true).trigger('input'); |
| 175 | $('#memory_prompt_words_force').val(extension_settings.memory.promptForceWords).trigger('input'); | 205 | $('#memory_prompt_words_force').val(extension_settings.memory.promptForceWords).trigger('input'); |
| 176 | $(`input[name="memory_prompt_builder"][value="${extension_settings.memory.prompt_builder}"]`).prop('checked', true).trigger('input'); | 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 | async function summarizeChatMain(context, force, skipWIAN) { | 782 | async function summarizeChatMain(context, force, skipWIAN) { |
| 709 | const prompt = await getSummaryPromptForNow(context, force); | 783 | const prompt = await getSummaryPromptForNow(context, force); |
| 710 | 784 | ||
| @@ -716,6 +790,48 @@ async function summarizeChatMain(context, force, skipWIAN) { | |||
| 716 | let summary = ''; | 790 | let summary = ''; |
| 717 | let index = null; | 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 | if (prompt_builders.DEFAULT === extension_settings.memory.prompt_builder) { | 835 | if (prompt_builders.DEFAULT === extension_settings.memory.prompt_builder) { |
| 720 | try { | 836 | try { |
| 721 | inApiCall = true; | 837 | inApiCall = true; |
| @@ -75,6 +75,9 @@ | |||
| 75 | <option value="1" data-i18n="User">User</option> | 75 | <option value="1" data-i18n="User">User</option> |
| 76 | </select> | 76 | </select> |
| 77 | <small data-i18n="ext_sum_prompt_role_desc">The role used to send the summary request instruction to the model.</small> | 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 | <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> | 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 | <input id="memory_prompt_words" type="range" value="{{defaultSettings.promptWords}}" min="{{defaultSettings.promptMinWords}}" max="{{defaultSettings.promptMaxWords}}" step="{{defaultSettings.promptWordsStep}}" /> | 82 | <input id="memory_prompt_words" type="range" value="{{defaultSettings.promptWords}}" min="{{defaultSettings.promptMinWords}}" max="{{defaultSettings.promptMaxWords}}" step="{{defaultSettings.promptWordsStep}}" /> |
| 80 | <label for="memory_override_response_length"> | 83 | <label for="memory_override_response_length"> |