Summarize: dedicated LLM connection profile for summarization Reviewed-by: auto-review

0320ffda52ab4b428632179444577f043defcc1f

permissionBRICK <permissionBRICK@gmail.com>

2 files changed, +120 -1Ignore whitespace
public/scripts/extensions/memory/index.js+117 -1
@@ -27,7 +27,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
27import { SlashCommand } from '../../slash-commands/SlashCommand.js';27import { SlashCommand } from '../../slash-commands/SlashCommand.js';
28import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';28import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
29import { macros, MacroCategory } from '../../macros/macro-system.js';29import { macros, MacroCategory } from '../../macros/macro-system.js';
30import { countWebLlmTokens, generateWebLlmChatPrompt, getWebLlmContextSize, isWebLlmSupported } from '../shared.js';30import { ConnectionManagerRequestService, countWebLlmTokens, generateWebLlmChatPrompt, getWebLlmContextSize, isWebLlmSupported } from '../shared.js';
31import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';31import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
32import { removeReasoningFromString } from '../../reasoning.js';32import { removeReasoningFromString } from '../../reasoning.js';
33import { MacrosParser } from '/scripts/macros.js';33import { 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};
141142
142/**143/**
@@ -150,6 +151,34 @@ function getSummaryPromptRole() {
150 : extension_prompt_roles.SYSTEM;151 : extension_prompt_roles.SYSTEM;
151}152}
152153
154let 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 */
161function 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
153function loadSettings() {182function 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}
707737
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 */
744function 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 */
759async 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
708async function summarizeChatMain(context, force, skipWIAN) {782async function summarizeChatMain(context, force, skipWIAN) {
709 const prompt = await getSummaryPromptForNow(context, force);783 const prompt = await getSummaryPromptForNow(context, force);
710784
@@ -716,6 +790,48 @@ async function summarizeChatMain(context, force, skipWIAN) {
716 let summary = '';790 let summary = '';
717 let index = null;791 let index = null;
718792
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;
public/scripts/extensions/memory/settings.html+3 -0
@@ -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">