Summarize: generate under the chosen profile via profile-switch + reconnect-wait (replaces sendRequest, which ignored the model)

03bd47c310151c3524cab3143c7aef2797018041

permissionBRICK <permissionBRICK@gmail.com>

1 files changed, +89 -86Showing whitespace changes
public/scripts/extensions/memory/index.js+89 -86
@@ -10,6 +10,7 @@ import {
1010 extension_prompt_types,
1111 generateQuietPrompt,
1212 is_send_press,
13+ online_status,
1314 saveSettingsDebounced,
1415 substituteParamsExtended,
1516 generateRaw,
@@ -735,48 +736,71 @@ async function summarizeChatWebLLM(context, force) {
735736 }
736737}
737738
738-/**
739+// Warn at most once per session if a summary connection profile is selected but cannot be
739- * Resolve the maximum response length (in tokens) to request from a connection profile.
740+// applied because there is no active connection profile to restore afterward.
740- * Uses the configured override when it is a positive number; otherwise falls back to a
741+let summaryProfileWarnedNoBaseProfile = false;
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-}
748742
749743/**
750744 * GenerateRuns a summarycallback usingwith a dedicatedspecific Connection Manager profile instead of thetemporarily active, model.then restores
751- * Assembles the chat transcript (via getRawSummaryPrompt) together with the summary instruction
745+ * the previously active profile. This lets the summary be generated by the chosen LLM using the
752- * so the chosen model actually receives the chat to summarize. Honors the configured summary
746+ * full summarization pipeline (generateQuietPrompt/generateRaw) under that connection, instead of
753747 * prompta rolecontext-free byConnection deliveringManager therequest instructionthat assome aproviders SYSTEMrun orwith USERthe messagewrong model.
754- * @param {object} context ST context
748+ *
755- * @param {string} profileId Connection Manager profile id
749+ * The switch is only performed when a real connection profile is currently active (so it can be
756- * @param {string} prompt Summary instruction
750+ * reliably restored). When none is active — i.e. the user drives the API panel manually —
757- * @returns {Promise<{summary: string, index: number}>} Generated summary and the index it covers up to
751+ * switching to a profile could not be undone without clobbering those manual settings, so we leave
752+ * the active model in place and warn once.
753+ *
754+ * @param {string} targetProfileId Profile to activate for the duration of the callback.
755+ * @param {() => Promise<any>} callback Work to run while the target profile is active.
756+ * @returns {Promise<any>} The callback's result.
758757 */
759758async function summarizeWithConnectionProfilewithConnectionProfile(context, profileIdtargetProfileId, promptcallback) {
760- const { rawPrompt, lastUsedIndex } = await getRawSummaryPrompt(context, prompt);
759+ const select = /** @type {HTMLSelectElement} */ (document.getElementById('connection_profiles'));
760+ const connectionManager = extension_settings.connectionManager;
761+ const currentProfileId = connectionManager?.selectedProfile;
762+
763+ const canSwitch = !!select
764+ && !!connectionManager
765+ && Array.isArray(connectionManager.profiles)
766+ && connectionManager.profiles.some(p => p.id === targetProfileId)
767+ && Array.from(select.options).some(o => o.value === targetProfileId)
768+ && !!currentProfileId // a real profile is active, so it can be restored afterwards
769+ && currentProfileId !== targetProfileId;
770+
771+ if (!canSwitch) {
772+ // Selected but no base profile to restore from -> use the active model and warn once.
773+ if (targetProfileId && !currentProfileId && !summaryProfileWarnedNoBaseProfile) {
774+ summaryProfileWarnedNoBaseProfile = true;
775+ toastr.info('The summary connection profile is only applied while a connection profile is active (so the original can be restored). Using the current model.', 'Summarize');
776+ }
777+ return await callback();
778+ }
779+
780+ const switchToProfile = async (profileId) => {
781+ const loaded = new Promise(resolve => eventSource.once(event_types.CONNECTION_PROFILE_LOADED, resolve));
782+ const timeout = new Promise(resolve => setTimeout(resolve, 10000));
783+ const index = Array.from(select.options).findIndex(o => o.value === profileId);
784+ select.selectedIndex = index >= 0 ? index : 0;
785+ select.dispatchEvent(new Event('change'));
786+ // Wait for the profile's commands to finish applying (don't hang forever if the event never fires).
787+ await Promise.race([loaded, timeout]);
788+ // Applying a profile reconnects the API asynchronously; generating before it is
789+ // re-established fails instantly, so wait for the connection to come back up
790+ // (mirrors the built-in /profile command). rejectOnTimeout:false -> proceed anyway after the timeout.
791+ await waitUntilCondition(() => online_status !== 'no_connection', 10000, 100, { rejectOnTimeout: false });
792+ };
761793
762- if (lastUsedIndex === null || lastUsedIndex === -1) {
794+ await switchToProfile(targetProfileId);
763- // Nothing to summarize; mirror the raw builder's guard.
795+ try {
764- return { summary: '', index: lastUsedIndex };
796+ return await callback();
797+ } finally {
798+ try {
799+ await switchToProfile(currentProfileId);
800+ } catch (err) {
801+ console.error('Summarize: failed to restore the previous connection profile after summarization', err);
802+ }
765803 }
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 };
780804}
781805
782806async function summarizeChatMain(context, force, skipWIAN) {
@@ -787,54 +811,14 @@ async function summarizeChatMain(context, force, skipWIAN) {
787811 }
788812
789813 console.log('sending summary prompt');
814+
815+ // Runs the configured summary builder (DEFAULT or RAW) against whatever connection is
816+ // currently active. Returns { summary, index }, or null when there is nothing to summarize.
817+ const runConfiguredSummary = async () => {
790818 let summary = '';
791819 let index = null;
792820
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-
835821 if (prompt_builders.DEFAULT === extension_settings.memory.prompt_builder) {
836- try {
837- inApiCall = true;
838822 // generateQuietPrompt always injects the instruction as a SYSTEM message
839823 // (the QUIET_PROMPT extension prompt has no exposed role parameter).
840824 // When a USER role is requested, assemble the chat transcript ourselves
@@ -859,15 +843,11 @@ async function summarizeChatMain(context, force, skipWIAN) {
859843 };
860844 summary = await generateQuietPrompt(params);
861845 }
862- } finally {
863- inApiCall = false;
864- }
865846 }
866847
867848 if ([prompt_builders.RAW_BLOCKING, prompt_builders.RAW_NON_BLOCKING].includes(extension_settings.memory.prompt_builder)) {
868849 const lock = extension_settings.memory.prompt_builder === prompt_builders.RAW_BLOCKING;
869850 try {
870- inApiCall = true;
871851 if (lock) {
872852 deactivateSendButtons();
873853 }
@@ -896,13 +876,36 @@ async function summarizeChatMain(context, force, skipWIAN) {
896876 summary = removeReasoningFromString(rawSummary);
897877 index = lastUsedIndex;
898878 } finally {
899- inApiCall = false;
900879 if (lock) {
901880 activateSendButtons();
902881 }
903882 }
904883 }
905884
885+ return { summary, index };
886+ };
887+
888+ // A dedicated connection profile generates the summary with the chosen LLM by temporarily
889+ // switching the active connection profile (so the full summarization pipeline runs under it),
890+ // then restoring the previous profile. Falls back to the active model when no profile is set
891+ // or no base profile is active to restore.
892+ const summaryConnectionProfile = extension_settings.memory.summaryConnectionProfile;
893+ let result;
894+ try {
895+ inApiCall = true;
896+ result = summaryConnectionProfile
897+ ? await withConnectionProfile(summaryConnectionProfile, runConfiguredSummary)
898+ : await runConfiguredSummary();
899+ } finally {
900+ inApiCall = false;
901+ }
902+
903+ if (result === null) {
904+ return null;
905+ }
906+
907+ const { summary, index } = result;
908+
906909 if (!summary) {
907910 console.warn('Empty summary received');
908911 return;