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

0320ffda52ab4b428632179444577f043defcc1f

permissionBRICK <permissionBRICK@gmail.com>

2 files changed, +120 -1Showing whitespace changes
public/scripts/extensions/memory/index.js+117 -1
@@ -27,7 +27,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
2727import { SlashCommand } from '../../slash-commands/SlashCommand.js';
2828import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
2929import { macros, MacroCategory } from '../../macros/macro-system.js';
3030import { ConnectionManagerRequestService, countWebLlmTokens, generateWebLlmChatPrompt, getWebLlmContextSize, isWebLlmSupported } from '../shared.js';
3131import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
3232import { removeReasoningFromString } from '../../reasoning.js';
3333import { MacrosParser } from '/scripts/macros.js';
@@ -137,6 +137,7 @@ const defaultSettings = {
137137 maxMessagesPerRequestStep: 1,
138138 prompt_builder: prompt_builders.DEFAULT,
139139 summaryPromptRole: extension_prompt_roles.SYSTEM,
140+ summaryConnectionProfile: '',
140141};
141142
142143/**
@@ -150,6 +151,34 @@ function getSummaryPromptRole() {
150151 : extension_prompt_roles.SYSTEM;
151152}
152153
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+
153182function loadSettings() {
154183 if (Object.keys(extension_settings.memory).length === 0) {
155184 Object.assign(extension_settings.memory, defaultSettings);
@@ -171,6 +200,7 @@ function loadSettings() {
171200 $('#memory_depth').val(extension_settings.memory.depth).trigger('input');
172201 $('#memory_role').val(extension_settings.memory.role).trigger('input');
173202 $('#memory_summary_prompt_role').val(extension_settings.memory.summaryPromptRole).trigger('input');
203+ initSummaryConnectionProfileDropdown();
174204 $(`input[name="memory_position"][value="${extension_settings.memory.position}"]`).prop('checked', true).trigger('input');
175205 $('#memory_prompt_words_force').val(extension_settings.memory.promptForceWords).trigger('input');
176206 $(`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) {
705735 }
706736}
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+ */
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+
708782async function summarizeChatMain(context, force, skipWIAN) {
709783 const prompt = await getSummaryPromptForNow(context, force);
710784
@@ -716,6 +790,48 @@ async function summarizeChatMain(context, force, skipWIAN) {
716790 let summary = '';
717791 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+
719835 if (prompt_builders.DEFAULT === extension_settings.memory.prompt_builder) {
720836 try {
721837 inApiCall = true;
public/scripts/extensions/memory/settings.html+3 -0
@@ -75,6 +75,9 @@
7575 <option value="1" data-i18n="User">User</option>
7676 </select>
7777 <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>
7881 <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>
7982 <input id="memory_prompt_words" type="range" value="{{defaultSettings.promptWords}}" min="{{defaultSettings.promptMinWords}}" max="{{defaultSettings.promptMaxWords}}" step="{{defaultSettings.promptWordsStep}}" />
8083 <label for="memory_override_response_length">