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, +120 -117Ignore whitespace
public/scripts/extensions/memory/index.js+120 -117
@@ -10,6 +10,7 @@ import {
10 extension_prompt_types,10 extension_prompt_types,
11 generateQuietPrompt,11 generateQuietPrompt,
12 is_send_press,12 is_send_press,
13 online_status,
13 saveSettingsDebounced,14 saveSettingsDebounced,
14 substituteParamsExtended,15 substituteParamsExtended,
15 generateRaw,16 generateRaw,
@@ -735,48 +736,71 @@ async function summarizeChatWebLLM(context, force) {
735 }736 }
736}737}
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 a741let summaryProfileWarnedNoBaseProfile = false;
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}
748742
749/**743/**
750 * Generate a summary using a dedicated Connection Manager profile instead of the active model.744 * Runs a callback with a specific Connection Manager profile temporarily active, then restores
751 * Assembles the chat transcript (via getRawSummaryPrompt) together with the summary instruction745 * 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 summary746 * full summarization pipeline (generateQuietPrompt/generateRaw) under that connection, instead of
753 * prompt role by delivering the instruction as a SYSTEM or USER message.747 * a context-free Connection Manager request that some providers run with the wrong model.
754 * @param {object} context ST context748 *
755 * @param {string} profileId Connection Manager profile id749 * The switch is only performed when a real connection profile is currently active (so it can be
756 * @param {string} prompt Summary instruction750 * 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 to751 * 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.
758 */757 */
759async function summarizeWithConnectionProfile(context, profileId, prompt) {758async function withConnectionProfile(targetProfileId, callback) {
760 const { rawPrompt, lastUsedIndex } = await getRawSummaryPrompt(context, prompt);759 const select = /** @type {HTMLSelectElement} */ (document.getElementById('connection_profiles'));
761760 const connectionManager = extension_settings.connectionManager;
762 if (lastUsedIndex === null || lastUsedIndex === -1) {761 const currentProfileId = connectionManager?.selectedProfile;
763 // Nothing to summarize; mirror the raw builder's guard.762
764 return { summary: '', index: lastUsedIndex };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();
765 }778 }
766779
767 const useUserRole = getSummaryPromptRole() === extension_prompt_roles.USER;780 const switchToProfile = async (profileId) => {
768 // sendRequest accepts an array of chat-completion messages, so deliver the instruction781 const loaded = new Promise(resolve => eventSource.once(event_types.CONNECTION_PROFILE_LOADED, resolve));
769 // as a separate SYSTEM or USER message (per the configured role) with the transcript as782 const timeout = new Promise(resolve => setTimeout(resolve, 10000));
770 // a USER message. For text-completion profiles these are concatenated downstream.783 const index = Array.from(select.options).findIndex(o => o.value === profileId);
771 const messages = useUserRole784 select.selectedIndex = index >= 0 ? index : 0;
772 ? [{ role: 'user', content: [prompt, rawPrompt].filter(x => x).join('\n\n') }]785 select.dispatchEvent(new Event('change'));
773 : [{ role: 'system', content: prompt }, { role: 'user', content: rawPrompt }];786 // Wait for the profile's commands to finish applying (don't hang forever if the event never fires).
774 const filteredMessages = messages.filter(m => m.content);787 await Promise.race([loaded, timeout]);
775788 // Applying a profile reconnects the API asynchronously; generating before it is
776 const data = await ConnectionManagerRequestService.sendRequest(profileId, filteredMessages, getSummaryProfileMaxTokens());789 // re-established fails instantly, so wait for the connection to come back up
777 // Non-streaming requests resolve to ExtractedData ({ content, reasoning }); extract the plain text.790 // (mirrors the built-in /profile command). rejectOnTimeout:false -> proceed anyway after the timeout.
778 const text = typeof data === 'object' && data !== null ? String(data.content ?? '') : '';791 await waitUntilCondition(() => online_status !== 'no_connection', 10000, 100, { rejectOnTimeout: false });
779 return { summary: removeReasoningFromString(text), index: lastUsedIndex };792 };
793
794 await switchToProfile(targetProfileId);
795 try {
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 }
803 }
780}804}
781805
782async function summarizeChatMain(context, force, skipWIAN) {806async function summarizeChatMain(context, force, skipWIAN) {
@@ -787,54 +811,14 @@ async function summarizeChatMain(context, force, skipWIAN) {
787 }811 }
788812
789 console.log('sending summary prompt');813 console.log('sending summary prompt');
790 let summary = '';
791 let index = null;
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 }
819814
820 if (profileSucceeded) {815 // Runs the configured summary builder (DEFAULT or RAW) against whatever connection is
821 if (!summary) {816 // currently active. Returns { summary, index }, or null when there is nothing to summarize.
822 console.warn('Empty summary received');817 const runConfiguredSummary = async () => {
823 return;818 let summary = '';
824 }819 let index = null;
825820
826 if (isContextChanged(context)) {821 if (prompt_builders.DEFAULT === extension_settings.memory.prompt_builder) {
827 return;
828 }
829
830 setMemoryContext(summary, true, index);
831 return summary;
832 }
833 }
834
835 if (prompt_builders.DEFAULT === extension_settings.memory.prompt_builder) {
836 try {
837 inApiCall = true;
838 // generateQuietPrompt always injects the instruction as a SYSTEM message822 // generateQuietPrompt always injects the instruction as a SYSTEM message
839 // (the QUIET_PROMPT extension prompt has no exposed role parameter).823 // (the QUIET_PROMPT extension prompt has no exposed role parameter).
840 // When a USER role is requested, assemble the chat transcript ourselves824 // When a USER role is requested, assemble the chat transcript ourselves
@@ -859,50 +843,69 @@ async function summarizeChatMain(context, force, skipWIAN) {
859 };843 };
860 summary = await generateQuietPrompt(params);844 summary = await generateQuietPrompt(params);
861 }845 }
862 } finally {
863 inApiCall = false;
864 }846 }
865 }
866847
867 if ([prompt_builders.RAW_BLOCKING, prompt_builders.RAW_NON_BLOCKING].includes(extension_settings.memory.prompt_builder)) {848 if ([prompt_builders.RAW_BLOCKING, prompt_builders.RAW_NON_BLOCKING].includes(extension_settings.memory.prompt_builder)) {
868 const lock = extension_settings.memory.prompt_builder === prompt_builders.RAW_BLOCKING;849 const lock = extension_settings.memory.prompt_builder === prompt_builders.RAW_BLOCKING;
869 try {850 try {
870 inApiCall = true;851 if (lock) {
871 if (lock) {852 deactivateSendButtons();
872 deactivateSendButtons();853 }
873 }
874854
875 const { rawPrompt, lastUsedIndex } = await getRawSummaryPrompt(context, prompt);855 const { rawPrompt, lastUsedIndex } = await getRawSummaryPrompt(context, prompt);
876856
877 if (lastUsedIndex === null || lastUsedIndex === -1) {857 if (lastUsedIndex === null || lastUsedIndex === -1) {
878 if (force) {858 if (force) {
879 toastr.info('To try again, remove the latest summary.', 'No messages found to summarize');859 toastr.info('To try again, remove the latest summary.', 'No messages found to summarize');
880 }860 }
881861
882 return null;862 return null;
883 }863 }
884864
885 // When the summary instruction should be a USER message, deliver it as865 // When the summary instruction should be a USER message, deliver it as
886 // user content and leave the system prompt empty. Otherwise keep the866 // user content and leave the system prompt empty. Otherwise keep the
887 // existing behavior of sending it as the system prompt.867 // existing behavior of sending it as the system prompt.
888 const useUserRole = getSummaryPromptRole() === extension_prompt_roles.USER;868 const useUserRole = getSummaryPromptRole() === extension_prompt_roles.USER;
889 /** @type {import('../../../script.js').GenerateRawParams} */869 /** @type {import('../../../script.js').GenerateRawParams} */
890 const params = {870 const params = {
891 prompt: useUserRole ? [prompt, rawPrompt].filter(x => x).join('\n\n') : rawPrompt,871 prompt: useUserRole ? [prompt, rawPrompt].filter(x => x).join('\n\n') : rawPrompt,
892 systemPrompt: useUserRole ? '' : prompt,872 systemPrompt: useUserRole ? '' : prompt,
893 responseLength: extension_settings.memory.overrideResponseLength,873 responseLength: extension_settings.memory.overrideResponseLength,
894 };874 };
895 const rawSummary = await generateRaw(params);875 const rawSummary = await generateRaw(params);
896 summary = removeReasoningFromString(rawSummary);876 summary = removeReasoningFromString(rawSummary);
897 index = lastUsedIndex;877 index = lastUsedIndex;
898 } finally {878 } finally {
899 inApiCall = false;879 if (lock) {
900 if (lock) {880 activateSendButtons();
901 activateSendButtons();881 }
902 }882 }
903 }883 }
884
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;
904 }905 }
905906
907 const { summary, index } = result;
908
906 if (!summary) {909 if (!summary) {
907 console.warn('Empty summary received');910 console.warn('Empty summary received');
908 return;911 return;