Summarize: generate under the chosen profile via profile-switch + reconnect-wait (replaces sendRequest, which ignored the model)
| @@ -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 | } |
| 737 | 738 | ||
| 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 | } | ||
| 748 | 742 | ||
| 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 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 |
| 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 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. | ||
| 758 | */ | 757 | */ |
| 759 | async function summarizeWithConnectionProfile(context, profileId, prompt) { | 758 | async function withConnectionProfile(targetProfileId, callback) { |
| 760 | const { rawPrompt, lastUsedIndex } = await getRawSummaryPrompt(context, prompt); | 759 | const select = /** @type {HTMLSelectElement} */ (document.getElementById('connection_profiles')); |
| 761 | 760 | 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 | } |
| 766 | 779 | ||
| 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 instruction | 781 | 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 as | 782 | 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 = useUserRole | 784 | 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]); |
| 775 | 788 | // 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 | } |
| 781 | 805 | ||
| 782 | async function summarizeChatMain(context, force, skipWIAN) { | 806 | async function summarizeChatMain(context, force, skipWIAN) { |
| @@ -787,54 +811,14 @@ async function summarizeChatMain(context, force, skipWIAN) { | |||
| 787 | } | 811 | } |
| 788 | 812 | ||
| 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 | } | ||
| 819 | 814 | ||
| 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; |
| 825 | 820 | ||
| 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 message | 822 | // 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 ourselves | 824 | // 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 | } | ||
| 866 | 847 | ||
| 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 | } | ||
| 874 | 854 | ||
| 875 | const { rawPrompt, lastUsedIndex } = await getRawSummaryPrompt(context, prompt); | 855 | const { rawPrompt, lastUsedIndex } = await getRawSummaryPrompt(context, prompt); |
| 876 | 856 | ||
| 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 | } |
| 881 | 861 | ||
| 882 | return null; | 862 | return null; |
| 883 | } | 863 | } |
| 884 | 864 | ||
| 885 | // When the summary instruction should be a USER message, deliver it as | 865 | // When the summary instruction should be a USER message, deliver it as |
| 886 | // user content and leave the system prompt empty. Otherwise keep the | 866 | // 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 | } |
| 905 | 906 | ||
| 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; |