Blame Raw
permissionBRICK · 03bd47c3 · · 1299 lines (49.6 KB)
4 contributors
1import { getStringHash, debounce, waitUntilCondition, extractAllWords, isTrueBoolean } from '../../utils.js';
2import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules, renderExtensionTemplateAsync } from '../../extensions.js';
3import {
4 activateSendButtons,
5 deactivateSendButtons,
6 animation_duration,
7 eventSource,
8 event_types,
9 extension_prompt_roles,
10 extension_prompt_types,
11 generateQuietPrompt,
12 is_send_press,
13 online_status,
14 saveSettingsDebounced,
15 substituteParamsExtended,
16 generateRaw,
17 getMaxPromptTokens,
18 setExtensionPrompt,
19 streamingProcessor,
20 animation_easing,
21} from '../../../script.js';
22import { is_group_generating, selected_group } from '../../group-chats.js';
23import { loadMovingUIState, power_user } from '../../power-user.js';
24import { dragElement } from '../../RossAscends-mods.js';
25import { getTextTokens, getTokenCountAsync, tokenizers } from '../../tokenizers.js';
26import { debounce_timeout } from '../../constants.js';
27import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
28import { SlashCommand } from '../../slash-commands/SlashCommand.js';
29import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
30import { macros, MacroCategory } from '../../macros/macro-system.js';
31import { ConnectionManagerRequestService, countWebLlmTokens, generateWebLlmChatPrompt, getWebLlmContextSize, isWebLlmSupported } from '../shared.js';
32import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
33import { removeReasoningFromString } from '../../reasoning.js';
34import { MacrosParser } from '/scripts/macros.js';
35export { MODULE_NAME };
36
37const MODULE_NAME = '1_memory';
38
39let lastMessageHash = null;
40let lastMessageId = null;
41let inApiCall = false;
42
43/**
44 * Count the number of tokens in the provided text.
45 * @param {string} text Text to count tokens for
46 * @param {number} padding Number of additional tokens to add to the count
47 * @returns {Promise<number>} Number of tokens in the text
48 */
49async function countSourceTokens(text, padding = 0) {
50 if (extension_settings.memory.source === summary_sources.webllm) {
51 const count = await countWebLlmTokens(text);
52 return count + padding;
53 }
54
55 if (extension_settings.memory.source === summary_sources.extras) {
56 const count = getTextTokens(tokenizers.GPT2, text).length;
57 return count + padding;
58 }
59
60 return await getTokenCountAsync(text, padding);
61}
62
63async function getSourceContextSize() {
64 const overrideLength = extension_settings.memory.overrideResponseLength;
65
66 if (extension_settings.memory.source === summary_sources.webllm) {
67 const maxContext = await getWebLlmContextSize();
68 return overrideLength > 0 ? (maxContext - overrideLength) : Math.round(maxContext * 0.75);
69 }
70
71 if (extension_settings.source === summary_sources.extras) {
72 return 1024 - 64;
73 }
74
75 return getMaxPromptTokens(overrideLength);
76}
77
78const formatMemoryValue = function (value) {
79 if (!value) {
80 return '';
81 }
82
83 value = value.trim();
84
85 if (extension_settings.memory.template) {
86 return substituteParamsExtended(extension_settings.memory.template, { summary: value });
87 } else {
88 return `Summary: ${value}`;
89 }
90};
91
92const saveChatDebounced = debounce(() => getContext().saveChat(), debounce_timeout.relaxed);
93
94const summary_sources = {
95 'extras': 'extras',
96 'main': 'main',
97 'webllm': 'webllm',
98};
99
100const prompt_builders = {
101 DEFAULT: 0,
102 RAW_BLOCKING: 1,
103 RAW_NON_BLOCKING: 2,
104};
105
106const defaultPrompt = 'Ignore previous instructions. Summarize the most important facts and events in the story so far. If a summary already exists in your memory, use that as a base and expand with new facts. Limit the summary to {{words}} words or less. Your response should include nothing but the summary.';
107const defaultTemplate = '[Summary: {{summary}}]';
108
109const defaultSettings = {
110 memoryFrozen: false,
111 SkipWIAN: false,
112 source: summary_sources.extras,
113 prompt: defaultPrompt,
114 template: defaultTemplate,
115 position: extension_prompt_types.IN_PROMPT,
116 role: extension_prompt_roles.SYSTEM,
117 scan: false,
118 depth: 2,
119 promptWords: 200,
120 promptMinWords: 25,
121 promptMaxWords: 1000,
122 promptWordsStep: 25,
123 promptInterval: 10,
124 promptMinInterval: 0,
125 promptMaxInterval: 250,
126 promptIntervalStep: 1,
127 promptForceWords: 0,
128 promptForceWordsStep: 100,
129 promptMinForceWords: 0,
130 promptMaxForceWords: 10000,
131 overrideResponseLength: 0,
132 overrideResponseLengthMin: 0,
133 overrideResponseLengthMax: 4096,
134 overrideResponseLengthStep: 16,
135 maxMessagesPerRequest: 0,
136 maxMessagesPerRequestMin: 0,
137 maxMessagesPerRequestMax: 250,
138 maxMessagesPerRequestStep: 1,
139 prompt_builder: prompt_builders.DEFAULT,
140 summaryPromptRole: extension_prompt_roles.SYSTEM,
141 summaryConnectionProfile: '',
142};
143
144/**
145 * Resolve the configured role for the summarization request prompt.
146 * Only SYSTEM and USER are supported; anything else falls back to SYSTEM.
147 * @returns {number} One of extension_prompt_roles.SYSTEM or extension_prompt_roles.USER
148 */
149function getSummaryPromptRole() {
150 return Number(extension_settings.memory.summaryPromptRole) === extension_prompt_roles.USER
151 ? extension_prompt_roles.USER
152 : extension_prompt_roles.SYSTEM;
153}
154
155let summaryConnectionProfileDropdownInitialized = false;
156
157/**
158 * Populates the dedicated summarization connection profile dropdown.
159 * Only initializes once to avoid attaching duplicate Connection Manager event listeners
160 * when loadSettings() is called again (e.g. after loading a settings preset).
161 */
162function initSummaryConnectionProfileDropdown() {
163 if (summaryConnectionProfileDropdownInitialized) {
164 return;
165 }
166
167 try {
168 ConnectionManagerRequestService.handleDropdown(
169 '#memory_summary_connection_profile',
170 extension_settings.memory.summaryConnectionProfile,
171 (profile) => {
172 extension_settings.memory.summaryConnectionProfile = profile?.id ?? '';
173 saveSettingsDebounced();
174 },
175 );
176 summaryConnectionProfileDropdownInitialized = true;
177 } catch (error) {
178 // Connection Manager may be unavailable/disabled; leave the dropdown empty in that case.
179 console.warn('Summarize: could not populate summary connection profile dropdown', error);
180 }
181}
182
183function loadSettings() {
184 if (Object.keys(extension_settings.memory).length === 0) {
185 Object.assign(extension_settings.memory, defaultSettings);
186 }
187
188 for (const key of Object.keys(defaultSettings)) {
189 if (extension_settings.memory[key] === undefined) {
190 extension_settings.memory[key] = defaultSettings[key];
191 }
192 }
193
194 $('#summary_source').val(extension_settings.memory.source).trigger('change');
195 $('#memory_frozen').prop('checked', extension_settings.memory.memoryFrozen).trigger('input');
196 $('#memory_skipWIAN').prop('checked', extension_settings.memory.SkipWIAN).trigger('input');
197 $('#memory_prompt').val(extension_settings.memory.prompt).trigger('input');
198 $('#memory_prompt_words').val(extension_settings.memory.promptWords).trigger('input');
199 $('#memory_prompt_interval').val(extension_settings.memory.promptInterval).trigger('input');
200 $('#memory_template').val(extension_settings.memory.template).trigger('input');
201 $('#memory_depth').val(extension_settings.memory.depth).trigger('input');
202 $('#memory_role').val(extension_settings.memory.role).trigger('input');
203 $('#memory_summary_prompt_role').val(extension_settings.memory.summaryPromptRole).trigger('input');
204 initSummaryConnectionProfileDropdown();
205 $(`input[name="memory_position"][value="${extension_settings.memory.position}"]`).prop('checked', true).trigger('input');
206 $('#memory_prompt_words_force').val(extension_settings.memory.promptForceWords).trigger('input');
207 $(`input[name="memory_prompt_builder"][value="${extension_settings.memory.prompt_builder}"]`).prop('checked', true).trigger('input');
208 $('#memory_override_response_length').val(extension_settings.memory.overrideResponseLength).trigger('input');
209 $('#memory_max_messages_per_request').val(extension_settings.memory.maxMessagesPerRequest).trigger('input');
210 $('#memory_include_wi_scan').prop('checked', extension_settings.memory.scan).trigger('input');
211 switchSourceControls(extension_settings.memory.source);
212}
213
214async function onPromptForceWordsAutoClick() {
215 const context = getContext();
216 const maxPromptLength = await getSourceContextSize();
217 const chat = context.chat;
218 const allMessages = chat.filter(m => !m.is_system && m.mes).map(m => m.mes);
219 const messagesWordCount = allMessages.map(m => extractAllWords(m)).flat().length;
220 const averageMessageWordCount = messagesWordCount / allMessages.length;
221 const tokensPerWord = await countSourceTokens(allMessages.join('\n')) / messagesWordCount;
222 const wordsPerToken = 1 / tokensPerWord;
223 const maxPromptLengthWords = Math.round(maxPromptLength * wordsPerToken);
224 // How many words should pass so that messages will start be dropped out of context;
225 const wordsPerPrompt = Math.floor(maxPromptLength / tokensPerWord);
226 // How many words will be needed to fit the allowance buffer
227 const summaryPromptWords = extractAllWords(extension_settings.memory.prompt).length;
228 const promptAllowanceWords = maxPromptLengthWords - extension_settings.memory.promptWords - summaryPromptWords;
229 const averageMessagesPerPrompt = Math.floor(promptAllowanceWords / averageMessageWordCount);
230 const maxMessagesPerSummary = extension_settings.memory.maxMessagesPerRequest || 0;
231 const targetMessagesInPrompt = maxMessagesPerSummary > 0 ? maxMessagesPerSummary : Math.max(0, averageMessagesPerPrompt);
232 const targetSummaryWords = (targetMessagesInPrompt * averageMessageWordCount) + (promptAllowanceWords / 4);
233
234 console.table({
235 maxPromptLength,
236 maxPromptLengthWords,
237 promptAllowanceWords,
238 averageMessagesPerPrompt,
239 targetMessagesInPrompt,
240 targetSummaryWords,
241 wordsPerPrompt,
242 wordsPerToken,
243 tokensPerWord,
244 messagesWordCount,
245 });
246
247 const ROUNDING = 100;
248 extension_settings.memory.promptForceWords = Math.max(1, Math.floor(targetSummaryWords / ROUNDING) * ROUNDING);
249 $('#memory_prompt_words_force').val(extension_settings.memory.promptForceWords).trigger('input');
250}
251
252async function onPromptIntervalAutoClick() {
253 const context = getContext();
254 const maxPromptLength = await getSourceContextSize();
255 const chat = context.chat;
256 const allMessages = chat.filter(m => !m.is_system && m.mes).map(m => m.mes);
257 const messagesWordCount = allMessages.map(m => extractAllWords(m)).flat().length;
258 const messagesTokenCount = await countSourceTokens(allMessages.join('\n'));
259 const tokensPerWord = messagesTokenCount / messagesWordCount;
260 const averageMessageTokenCount = messagesTokenCount / allMessages.length;
261 const targetSummaryTokens = Math.round(extension_settings.memory.promptWords * tokensPerWord);
262 const promptTokens = await countSourceTokens(extension_settings.memory.prompt);
263 const promptAllowance = maxPromptLength - promptTokens - targetSummaryTokens;
264 const maxMessagesPerSummary = extension_settings.memory.maxMessagesPerRequest || 0;
265 const averageMessagesPerPrompt = Math.floor(promptAllowance / averageMessageTokenCount);
266 const targetMessagesInPrompt = maxMessagesPerSummary > 0 ? maxMessagesPerSummary : Math.max(0, averageMessagesPerPrompt);
267 const adjustedAverageMessagesPerPrompt = targetMessagesInPrompt + (averageMessagesPerPrompt - targetMessagesInPrompt) / 4;
268
269 console.table({
270 maxPromptLength,
271 promptAllowance,
272 targetSummaryTokens,
273 promptTokens,
274 messagesWordCount,
275 messagesTokenCount,
276 tokensPerWord,
277 averageMessageTokenCount,
278 averageMessagesPerPrompt,
279 targetMessagesInPrompt,
280 adjustedAverageMessagesPerPrompt,
281 maxMessagesPerSummary,
282 });
283
284 const ROUNDING = 5;
285 extension_settings.memory.promptInterval = Math.max(1, Math.floor(adjustedAverageMessagesPerPrompt / ROUNDING) * ROUNDING);
286
287 $('#memory_prompt_interval').val(extension_settings.memory.promptInterval).trigger('input');
288}
289
290function onSummarySourceChange(event) {
291 const value = event.target.value;
292 extension_settings.memory.source = value;
293 switchSourceControls(value);
294 saveSettingsDebounced();
295}
296
297function switchSourceControls(value) {
298 $('#summaryExtensionDrawerContents [data-summary-source], #memory_settings [data-summary-source]').each((_, element) => {
299 const source = element.dataset.summarySource.split(',').map(s => s.trim());
300 $(element).toggle(source.includes(value));
301 });
302}
303
304function onMemoryFrozenInput() {
305 const value = Boolean($(this).prop('checked'));
306 extension_settings.memory.memoryFrozen = value;
307 saveSettingsDebounced();
308}
309
310function onMemorySkipWIANInput() {
311 const value = Boolean($(this).prop('checked'));
312 extension_settings.memory.SkipWIAN = value;
313 saveSettingsDebounced();
314}
315
316function onMemoryPromptWordsInput() {
317 const value = $(this).val();
318 extension_settings.memory.promptWords = Number(value);
319 $('#memory_prompt_words_value').text(extension_settings.memory.promptWords);
320 saveSettingsDebounced();
321}
322
323function onMemoryPromptIntervalInput() {
324 const value = $(this).val();
325 extension_settings.memory.promptInterval = Number(value);
326 $('#memory_prompt_interval_value').text(extension_settings.memory.promptInterval);
327 saveSettingsDebounced();
328}
329
330function onMemoryPromptRestoreClick() {
331 $('#memory_prompt').val(defaultPrompt).trigger('input');
332}
333
334function onMemoryPromptInput() {
335 const value = $(this).val();
336 extension_settings.memory.prompt = value;
337 saveSettingsDebounced();
338}
339
340function onMemoryTemplateInput() {
341 const value = $(this).val();
342 extension_settings.memory.template = value;
343 reinsertMemory();
344 saveSettingsDebounced();
345}
346
347function onMemoryDepthInput() {
348 const value = $(this).val();
349 extension_settings.memory.depth = Number(value);
350 reinsertMemory();
351 saveSettingsDebounced();
352}
353
354function onMemoryRoleInput() {
355 const value = $(this).val();
356 extension_settings.memory.role = Number(value);
357 reinsertMemory();
358 saveSettingsDebounced();
359}
360
361function onMemorySummaryPromptRoleInput() {
362 const value = $(this).val();
363 extension_settings.memory.summaryPromptRole = Number(value);
364 saveSettingsDebounced();
365}
366
367function onMemoryPositionChange(e) {
368 const value = e.target.value;
369 extension_settings.memory.position = value;
370 reinsertMemory();
371 saveSettingsDebounced();
372}
373
374function onMemoryIncludeWIScanInput() {
375 const value = !!$(this).prop('checked');
376 extension_settings.memory.scan = value;
377 reinsertMemory();
378 saveSettingsDebounced();
379}
380
381function onMemoryPromptWordsForceInput() {
382 const value = $(this).val();
383 extension_settings.memory.promptForceWords = Number(value);
384 $('#memory_prompt_words_force_value').text(extension_settings.memory.promptForceWords);
385 saveSettingsDebounced();
386}
387
388function onOverrideResponseLengthInput() {
389 const value = $(this).val();
390 extension_settings.memory.overrideResponseLength = Number(value);
391 $('#memory_override_response_length_value').text(extension_settings.memory.overrideResponseLength);
392 saveSettingsDebounced();
393}
394
395function onMaxMessagesPerRequestInput() {
396 const value = $(this).val();
397 extension_settings.memory.maxMessagesPerRequest = Number(value);
398 $('#memory_max_messages_per_request_value').text(extension_settings.memory.maxMessagesPerRequest);
399 saveSettingsDebounced();
400}
401
402/**
403 * Get the latest memory summary from the chat.
404 * @param {ChatMessage[]} chat Chat messages
405 * @returns {string} Latest memory summary or empty string
406 */
407function getLatestMemoryFromChat(chat) {
408 if (!Array.isArray(chat) || !chat.length) {
409 return '';
410 }
411
412 const reversedChat = chat.slice().reverse();
413 reversedChat.shift();
414 for (let mes of reversedChat) {
415 if (mes.extra && mes.extra.memory) {
416 return mes.extra.memory;
417 }
418 }
419
420 return '';
421}
422
423/**
424 * Get the index of the latest memory summary from the chat.
425 * @param {ChatMessage[]} chat Chat messages
426 * @returns {number} Index of the latest memory summary or -1 if not found
427 */
428function getIndexOfLatestChatSummary(chat) {
429 if (!Array.isArray(chat) || !chat.length) {
430 return -1;
431 }
432
433 const reversedChat = chat.slice().reverse();
434 reversedChat.shift();
435 for (let mes of reversedChat) {
436 if (mes.extra && mes.extra.memory) {
437 return chat.indexOf(mes);
438 }
439 }
440
441 return -1;
442}
443
444/**
445 * Check if something is changed during the summarization process.
446 * @param {{ groupId: any; chatId: any; characterId: any; }} context
447 * @returns {boolean} True if the context has changed and the summary should be discarded
448 */
449function isContextChanged(context) {
450 const newContext = getContext();
451 if (newContext.groupId !== context.groupId
452 || newContext.chatId !== context.chatId
453 || (!newContext.groupId && (newContext.characterId !== context.characterId))) {
454 console.log('Context changed, summary discarded');
455 return true;
456 }
457
458 return false;
459}
460
461function onChatChanged() {
462 const context = getContext();
463 const latestMemory = getLatestMemoryFromChat(context.chat);
464 setMemoryContext(latestMemory, false);
465}
466
467async function onChatEvent() {
468 // Module not enabled
469 if (extension_settings.memory.source === summary_sources.extras && !modules.includes('summarize')) {
470 return;
471 }
472
473 // WebLLM is not supported
474 if (extension_settings.memory.source === summary_sources.webllm && !isWebLlmSupported()) {
475 return;
476 }
477
478 // Streaming in-progress
479 if (streamingProcessor && !streamingProcessor.isFinished) {
480 return;
481 }
482
483 // Currently summarizing or frozen state - skip
484 if (inApiCall || extension_settings.memory.memoryFrozen) {
485 return;
486 }
487
488 const context = getContext();
489 const chat = context.chat;
490 // Chat can't be empty.
491 if (chat.length === 0) return;
492
493 const lastMessage = chat[chat.length - 1];
494
495 // No new messages - do nothing
496 if ((lastMessageId === chat.length && getStringHash(lastMessage.mes) === lastMessageHash)) {
497 return;
498 }
499
500 // Messages has been deleted - rewrite the context with the latest available memory
501 if (chat.length < lastMessageId) {
502 const latestMemory = getLatestMemoryFromChat(chat);
503 setMemoryContext(latestMemory, false);
504 }
505
506 // Message has been edited / regenerated - delete the saved memory
507 if (chat.length
508 && lastMessage.extra
509 && lastMessage.extra.memory
510 && lastMessageId === chat.length
511 && getStringHash(lastMessage.mes) !== lastMessageHash) {
512 delete lastMessage.extra.memory;
513 }
514
515 summarizeChat(context)
516 .catch(console.error)
517 .finally(() => {
518 lastMessageId = context.chat?.length ?? null;
519 lastMessageHash = getStringHash((context.chat.length && context.chat[context.chat.length - 1].mes) ?? '');
520 });
521}
522
523/**
524 * Forces a summary generation for the current chat.
525 * @param {boolean} quiet If an informational toast should be displayed
526 * @returns {Promise<string>} Summarized text
527 */
528async function forceSummarizeChat(quiet) {
529 if (extension_settings.memory.source === summary_sources.extras) {
530 toastr.warning('Force summarization is not supported for Extras API');
531 return;
532 }
533
534 const context = getContext();
535 const skipWIAN = extension_settings.memory.SkipWIAN;
536
537 const toast = quiet ? jQuery() : toastr.info('Summarizing chat...', 'Please wait', { timeOut: 0, extendedTimeOut: 0 });
538 const value = extension_settings.memory.source === summary_sources.main
539 ? await summarizeChatMain(context, true, skipWIAN)
540 : await summarizeChatWebLLM(context, true);
541
542 toastr.clear(toast);
543
544 if (!value) {
545 toastr.warning('Failed to summarize chat');
546 return '';
547 }
548
549 return value;
550}
551
552/**
553 * Callback for the summarize command.
554 * @param {object} args Command arguments
555 * @param {string} text Text to summarize
556 */
557async function summarizeCallback(args, text) {
558 text = text.trim();
559
560 // Summarize the current chat if no text provided
561 if (!text) {
562 const quiet = isTrueBoolean(args.quiet);
563 return await forceSummarizeChat(quiet);
564 }
565
566 const source = args.source || extension_settings.memory.source;
567 const prompt = substituteParamsExtended((args.prompt || extension_settings.memory.prompt), { words: extension_settings.memory.promptWords });
568 const useUserRole = getSummaryPromptRole() === extension_prompt_roles.USER;
569
570 try {
571 switch (source) {
572 case summary_sources.extras:
573 return await callExtrasSummarizeAPI(text);
574 case summary_sources.main: {
575 // When the instruction should be a USER message, combine it into the
576 // user content and clear the system prompt. Otherwise keep it as system.
577 const rawPrompt = useUserRole ? [prompt, text].filter(x => x).join('\n\n') : text;
578 const systemPrompt = useUserRole ? '' : prompt;
579 return removeReasoningFromString(await generateRaw({ prompt: rawPrompt, systemPrompt: systemPrompt, responseLength: extension_settings.memory.overrideResponseLength }));
580 }
581 case summary_sources.webllm: {
582 const promptRole = useUserRole ? 'user' : 'system';
583 const messages = [{ role: promptRole, content: prompt }, { role: 'user', content: text }].filter(m => m.content);
584 const params = extension_settings.memory.overrideResponseLength > 0 ? { max_tokens: extension_settings.memory.overrideResponseLength } : {};
585 return await generateWebLlmChatPrompt(messages, params);
586 }
587 default:
588 toastr.warning('Invalid summarization source specified');
589 return '';
590 }
591 } catch (error) {
592 toastr.error(String(error), 'Failed to summarize text');
593 console.log(error);
594 return '';
595 }
596}
597
598async function summarizeChat(context) {
599 const skipWIAN = extension_settings.memory.SkipWIAN;
600 switch (extension_settings.memory.source) {
601 case summary_sources.extras:
602 await summarizeChatExtras(context);
603 break;
604 case summary_sources.main:
605 await summarizeChatMain(context, false, skipWIAN);
606 break;
607 case summary_sources.webllm:
608 await summarizeChatWebLLM(context, false);
609 break;
610 default:
611 break;
612 }
613}
614
615/**
616 * Check if the chat should be summarized based on the current conditions.
617 * Return summary prompt if it should be summarized.
618 * @param {any} context ST context
619 * @param {boolean} force Summarize the chat regardless of the conditions
620 * @returns {Promise<string>} Summary prompt or empty string
621 */
622async function getSummaryPromptForNow(context, force) {
623 if (extension_settings.memory.promptInterval === 0 && !force) {
624 console.debug('Prompt interval is set to 0, skipping summarization');
625 return '';
626 }
627
628 try {
629 // Wait for group to finish generating
630 if (selected_group) {
631 await waitUntilCondition(() => is_group_generating === false, 1000, 10);
632 }
633 // Wait for the send button to be released
634 await waitUntilCondition(() => is_send_press === false, 30000, 100);
635 } catch {
636 console.debug('Timeout waiting for is_send_press');
637 return '';
638 }
639
640 if (!context.chat.length) {
641 console.debug('No messages in chat to summarize');
642 return '';
643 }
644
645 if (context.chat.length < extension_settings.memory.promptInterval && !force) {
646 console.debug(`Not enough messages in chat to summarize (chat: ${context.chat.length}, interval: ${extension_settings.memory.promptInterval})`);
647 return '';
648 }
649
650 let messagesSinceLastSummary = 0;
651 let wordsSinceLastSummary = 0;
652 let conditionSatisfied = false;
653 for (let i = context.chat.length - 1; i >= 0; i--) {
654 if (context.chat[i].extra && context.chat[i].extra.memory) {
655 break;
656 }
657 messagesSinceLastSummary++;
658 wordsSinceLastSummary += extractAllWords(context.chat[i].mes).length;
659 }
660
661 if (messagesSinceLastSummary >= extension_settings.memory.promptInterval) {
662 conditionSatisfied = true;
663 }
664
665 if (extension_settings.memory.promptForceWords && wordsSinceLastSummary >= extension_settings.memory.promptForceWords) {
666 conditionSatisfied = true;
667 }
668
669 if (!conditionSatisfied && !force) {
670 console.debug(`Summary conditions not satisfied (messages: ${messagesSinceLastSummary}, interval: ${extension_settings.memory.promptInterval}, words: ${wordsSinceLastSummary}, force words: ${extension_settings.memory.promptForceWords})`);
671 return '';
672 }
673
674 console.log('Summarizing chat, messages since last summary: ' + messagesSinceLastSummary, 'words since last summary: ' + wordsSinceLastSummary);
675 const prompt = substituteParamsExtended(extension_settings.memory.prompt, { words: extension_settings.memory.promptWords });
676
677 if (!prompt) {
678 console.debug('Summarization prompt is empty. Skipping summarization.');
679 return '';
680 }
681
682 return prompt;
683}
684
685async function summarizeChatWebLLM(context, force) {
686 if (!isWebLlmSupported()) {
687 return;
688 }
689
690 const prompt = await getSummaryPromptForNow(context, force);
691
692 if (!prompt) {
693 return;
694 }
695
696 const { rawPrompt, lastUsedIndex } = await getRawSummaryPrompt(context, prompt);
697
698 if (lastUsedIndex === null || lastUsedIndex === -1) {
699 if (force) {
700 toastr.info('To try again, remove the latest summary.', 'No messages found to summarize');
701 }
702
703 return null;
704 }
705
706 const promptRole = getSummaryPromptRole() === extension_prompt_roles.USER ? 'user' : 'system';
707 const messages = [
708 { role: promptRole, content: prompt },
709 { role: 'user', content: rawPrompt },
710 ];
711
712 const params = {};
713
714 if (extension_settings.memory.overrideResponseLength > 0) {
715 params.max_tokens = extension_settings.memory.overrideResponseLength;
716 }
717
718 try {
719 inApiCall = true;
720 const summary = await generateWebLlmChatPrompt(messages, params);
721
722 if (!summary) {
723 console.warn('Empty summary received');
724 return;
725 }
726
727 // something changed during summarization request
728 if (isContextChanged(context)) {
729 return;
730 }
731
732 setMemoryContext(summary, true, lastUsedIndex);
733 return summary;
734 } finally {
735 inApiCall = false;
736 }
737}
738
739// Warn at most once per session if a summary connection profile is selected but cannot be
740// applied because there is no active connection profile to restore afterward.
741let summaryProfileWarnedNoBaseProfile = false;
742
743/**
744 * Runs a callback with a specific Connection Manager profile temporarily active, then restores
745 * the previously active profile. This lets the summary be generated by the chosen LLM using the
746 * full summarization pipeline (generateQuietPrompt/generateRaw) under that connection, instead of
747 * a context-free Connection Manager request that some providers run with the wrong model.
748 *
749 * The switch is only performed when a real connection profile is currently active (so it can be
750 * reliably restored). When none is active — i.e. the user drives the API panel manually —
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.
757 */
758async function withConnectionProfile(targetProfileId, callback) {
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 };
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 }
804}
805
806async function summarizeChatMain(context, force, skipWIAN) {
807 const prompt = await getSummaryPromptForNow(context, force);
808
809 if (!prompt) {
810 return;
811 }
812
813 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 () => {
818 let summary = '';
819 let index = null;
820
821 if (prompt_builders.DEFAULT === extension_settings.memory.prompt_builder) {
822 // generateQuietPrompt always injects the instruction as a SYSTEM message
823 // (the QUIET_PROMPT extension prompt has no exposed role parameter).
824 // When a USER role is requested, assemble the chat transcript ourselves
825 // (same source as the raw builder) and deliver the instruction as USER
826 // content via generateRaw, so the model still receives the chat to
827 // summarize instead of just the bare instruction.
828 if (getSummaryPromptRole() === extension_prompt_roles.USER) {
829 const { rawPrompt } = await getRawSummaryPrompt(context, prompt);
830 /** @type {import('../../../script.js').GenerateRawParams} */
831 const params = {
832 prompt: [prompt, rawPrompt].filter(x => x).join('\n\n'),
833 systemPrompt: '',
834 responseLength: extension_settings.memory.overrideResponseLength,
835 };
836 summary = removeReasoningFromString(await generateRaw(params));
837 } else {
838 /** @type {import('../../../script.js').GenerateQuietPromptParams} */
839 const params = {
840 quietPrompt: prompt,
841 skipWIAN: skipWIAN,
842 responseLength: extension_settings.memory.overrideResponseLength,
843 };
844 summary = await generateQuietPrompt(params);
845 }
846 }
847
848 if ([prompt_builders.RAW_BLOCKING, prompt_builders.RAW_NON_BLOCKING].includes(extension_settings.memory.prompt_builder)) {
849 const lock = extension_settings.memory.prompt_builder === prompt_builders.RAW_BLOCKING;
850 try {
851 if (lock) {
852 deactivateSendButtons();
853 }
854
855 const { rawPrompt, lastUsedIndex } = await getRawSummaryPrompt(context, prompt);
856
857 if (lastUsedIndex === null || lastUsedIndex === -1) {
858 if (force) {
859 toastr.info('To try again, remove the latest summary.', 'No messages found to summarize');
860 }
861
862 return null;
863 }
864
865 // When the summary instruction should be a USER message, deliver it as
866 // user content and leave the system prompt empty. Otherwise keep the
867 // existing behavior of sending it as the system prompt.
868 const useUserRole = getSummaryPromptRole() === extension_prompt_roles.USER;
869 /** @type {import('../../../script.js').GenerateRawParams} */
870 const params = {
871 prompt: useUserRole ? [prompt, rawPrompt].filter(x => x).join('\n\n') : rawPrompt,
872 systemPrompt: useUserRole ? '' : prompt,
873 responseLength: extension_settings.memory.overrideResponseLength,
874 };
875 const rawSummary = await generateRaw(params);
876 summary = removeReasoningFromString(rawSummary);
877 index = lastUsedIndex;
878 } finally {
879 if (lock) {
880 activateSendButtons();
881 }
882 }
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;
905 }
906
907 const { summary, index } = result;
908
909 if (!summary) {
910 console.warn('Empty summary received');
911 return;
912 }
913
914 if (isContextChanged(context)) {
915 return;
916 }
917
918 setMemoryContext(summary, true, index);
919 return summary;
920}
921
922/**
923 * Get the raw summarization prompt from the chat context.
924 * @param {object} context ST context
925 * @param {string} prompt Summarization system prompt
926 * @returns {Promise<{rawPrompt: string, lastUsedIndex: number}>} Raw summarization prompt
927 */
928async function getRawSummaryPrompt(context, prompt) {
929 /**
930 * Get the memory string from the chat buffer.
931 * @param {boolean} includeSystem Include prompt into the memory string
932 * @returns {string} Memory string
933 */
934 function getMemoryString(includeSystem) {
935 const delimiter = '\n\n';
936 const stringBuilder = [];
937 const bufferString = chatBuffer.slice().join(delimiter);
938
939 if (includeSystem) {
940 stringBuilder.push(prompt);
941 }
942
943 if (latestSummary) {
944 stringBuilder.push(latestSummary);
945 }
946
947 stringBuilder.push(bufferString);
948
949 return stringBuilder.join(delimiter).trim();
950 }
951
952 const chat = context.chat.slice();
953 const latestSummary = getLatestMemoryFromChat(chat);
954 const latestSummaryIndex = getIndexOfLatestChatSummary(chat);
955 chat.pop(); // We always exclude the last message from the buffer
956 const chatBuffer = [];
957 const PADDING = 64;
958 const PROMPT_SIZE = await getSourceContextSize();
959 let latestUsedMessage = null;
960
961 for (let index = latestSummaryIndex + 1; index < chat.length; index++) {
962 const message = chat[index];
963
964 if (!message) {
965 break;
966 }
967
968 if (message.is_system || !message.mes) {
969 continue;
970 }
971
972 const entry = `${message.name}:\n${message.mes}`;
973 chatBuffer.push(entry);
974
975 const tokens = await countSourceTokens(getMemoryString(true), PADDING);
976
977 if (tokens > PROMPT_SIZE) {
978 chatBuffer.pop();
979 break;
980 }
981
982 latestUsedMessage = message;
983
984 if (extension_settings.memory.maxMessagesPerRequest > 0 && chatBuffer.length >= extension_settings.memory.maxMessagesPerRequest) {
985 break;
986 }
987 }
988
989 const lastUsedIndex = context.chat.indexOf(latestUsedMessage);
990 const rawPrompt = getMemoryString(false);
991 return { rawPrompt, lastUsedIndex };
992}
993
994async function summarizeChatExtras(context) {
995 function getMemoryString() {
996 return (longMemory + '\n\n' + memoryBuffer.slice().reverse().join('\n\n')).trim();
997 }
998
999 const chat = context.chat;
1000 const longMemory = getLatestMemoryFromChat(chat);
1001 const reversedChat = chat.slice().reverse();
1002 reversedChat.shift();
1003 const memoryBuffer = [];
1004 const CONTEXT_SIZE = await getSourceContextSize();
1005
1006 for (const message of reversedChat) {
1007 // we reached the point of latest memory
1008 if (longMemory && message.extra && message.extra.memory == longMemory) {
1009 break;
1010 }
1011
1012 // don't care about system
1013 if (message.is_system) {
1014 continue;
1015 }
1016
1017 // determine the sender's name
1018 const entry = `${message.name}:\n${message.mes}`;
1019 memoryBuffer.push(entry);
1020
1021 // check if token limit was reached
1022 const tokens = await countSourceTokens(getMemoryString());
1023 if (tokens >= CONTEXT_SIZE) {
1024 break;
1025 }
1026 }
1027
1028 const resultingString = getMemoryString();
1029 const resultingTokens = await countSourceTokens(resultingString);
1030
1031 if (!resultingString || resultingTokens < CONTEXT_SIZE) {
1032 console.debug('Not enough context to summarize');
1033 return;
1034 }
1035
1036 // perform the summarization API call
1037 try {
1038 inApiCall = true;
1039 const summary = await callExtrasSummarizeAPI(resultingString);
1040
1041 if (!summary) {
1042 console.warn('Empty summary received');
1043 return;
1044 }
1045
1046 if (isContextChanged(context)) {
1047 return;
1048 }
1049
1050 setMemoryContext(summary, true);
1051 } catch (error) {
1052 console.log(error);
1053 } finally {
1054 inApiCall = false;
1055 }
1056}
1057
1058/**
1059 * Call the Extras API to summarize the provided text.
1060 * @param {string} text Text to summarize
1061 * @returns {Promise<string>} Summarized text
1062 */
1063async function callExtrasSummarizeAPI(text) {
1064 if (!modules.includes('summarize')) {
1065 throw new Error('Summarize module is not enabled in Extras API');
1066 }
1067
1068 const url = new URL(getApiUrl());
1069 url.pathname = '/api/summarize';
1070
1071 const apiResult = await doExtrasFetch(url, {
1072 method: 'POST',
1073 headers: {
1074 'Content-Type': 'application/json',
1075 'Bypass-Tunnel-Reminder': 'bypass',
1076 },
1077 body: JSON.stringify({
1078 text: text,
1079 params: {},
1080 }),
1081 });
1082
1083 if (apiResult.ok) {
1084 const data = await apiResult.json();
1085 const summary = data.summary;
1086 return summary;
1087 }
1088
1089 throw new Error('Extras API call failed');
1090}
1091
1092function onMemoryRestoreClick() {
1093 const context = getContext();
1094 const content = $('#memory_contents').val();
1095 const reversedChat = context.chat.slice().reverse();
1096 reversedChat.shift();
1097
1098 for (let mes of reversedChat) {
1099 if (mes.extra && mes.extra.memory == content) {
1100 delete mes.extra.memory;
1101 break;
1102 }
1103 }
1104
1105 const newContent = getLatestMemoryFromChat(context.chat);
1106 setMemoryContext(newContent, false);
1107}
1108
1109function onMemoryContentInput() {
1110 const value = $(this).val();
1111 setMemoryContext(value, true);
1112}
1113
1114function onMemoryPromptBuilderInput(e) {
1115 const value = Number(e.target.value);
1116 extension_settings.memory.prompt_builder = value;
1117 saveSettingsDebounced();
1118}
1119
1120function reinsertMemory() {
1121 const existingValue = String($('#memory_contents').val());
1122 setMemoryContext(existingValue, false);
1123}
1124
1125/**
1126 * Set the summary value to the context and save it to the chat message extra.
1127 * @param {string} value Value of a summary
1128 * @param {boolean} saveToMessage Should the summary be saved to the chat message extra
1129 * @param {number|null} index Index of the chat message to save the summary to. If null, the pre-last message is used.
1130 */
1131function setMemoryContext(value, saveToMessage, index = null) {
1132 setExtensionPrompt(MODULE_NAME, formatMemoryValue(value), extension_settings.memory.position, extension_settings.memory.depth, extension_settings.memory.scan, extension_settings.memory.role);
1133 $('#memory_contents').val(value);
1134
1135 const summaryLog = value
1136 ? `Summary set to: ${value}. Position: ${extension_settings.memory.position}. Depth: ${extension_settings.memory.depth}. Role: ${extension_settings.memory.role}`
1137 : 'Summary has no content';
1138 console.debug(summaryLog);
1139
1140 const context = getContext();
1141 if (saveToMessage && context.chat.length) {
1142 const idx = index ?? context.chat.length - 2;
1143 const mes = context.chat[idx < 0 ? 0 : idx];
1144
1145 if (!mes.extra) {
1146 mes.extra = {};
1147 }
1148
1149 mes.extra.memory = value;
1150 saveChatDebounced();
1151 }
1152}
1153
1154function doPopout(e) {
1155 const target = e.target;
1156 //repurposes the zoomed avatar template to server as a floating div
1157 if ($('#summaryExtensionPopout').length === 0) {
1158 console.debug('did not see popout yet, creating');
1159 const originalHTMLClone = $(target).parent().parent().parent().find('.inline-drawer-content').html();
1160 const originalElement = $(target).parent().parent().parent().find('.inline-drawer-content');
1161 const template = $('#zoomed_avatar_template').html();
1162 const controlBarHtml = `<div class="panelControlBar flex-container">
1163 <div id="summaryExtensionPopoutheader" class="fa-solid fa-grip drag-grabber hoverglow"></div>
1164 <div id="summaryExtensionPopoutClose" class="fa-solid fa-circle-xmark hoverglow dragClose"></div>
1165 </div>`;
1166 const newElement = $(template);
1167 newElement.attr('id', 'summaryExtensionPopout')
1168 .css('opacity', 0)
1169 .removeClass('zoomed_avatar')
1170 .addClass('draggable')
1171 .empty();
1172 const prevSummaryBoxContents = $('#memory_contents').val().toString(); //copy summary box before emptying
1173 originalElement.empty();
1174 originalElement.html('<div class="flex-container alignitemscenter justifyCenter wide100p"><small>Currently popped out</small></div>');
1175 newElement.append(controlBarHtml).append(originalHTMLClone);
1176 $('#movingDivs').append(newElement);
1177 newElement.transition({ opacity: 1, duration: animation_duration, easing: animation_easing });
1178 $('#summaryExtensionDrawerContents').addClass('scrollableInnerFull');
1179 setMemoryContext(prevSummaryBoxContents, false); //paste prev summary box contents into popout box
1180 setupListeners();
1181 loadSettings();
1182 loadMovingUIState();
1183
1184 dragElement(newElement);
1185
1186 //setup listener for close button to restore extensions menu
1187 $('#summaryExtensionPopoutClose').off('click').on('click', function () {
1188 $('#summaryExtensionDrawerContents').removeClass('scrollableInnerFull');
1189 const summaryPopoutHTML = $('#summaryExtensionDrawerContents');
1190 $('#summaryExtensionPopout').fadeOut(animation_duration, () => {
1191 originalElement.empty();
1192 originalElement.append(summaryPopoutHTML);
1193 $('#summaryExtensionPopout').remove();
1194 });
1195 loadSettings();
1196 });
1197 } else {
1198 console.debug('saw existing popout, removing');
1199 $('#summaryExtensionPopout').fadeOut(animation_duration, () => { $('#summaryExtensionPopoutClose').trigger('click'); });
1200 }
1201}
1202
1203function setupListeners() {
1204 //setup shared listeners for popout and regular ext menu
1205 $('#memory_restore').off('click').on('click', onMemoryRestoreClick);
1206 $('#memory_contents').off('input').on('input', onMemoryContentInput);
1207 $('#memory_frozen').off('input').on('input', onMemoryFrozenInput);
1208 $('#memory_skipWIAN').off('input').on('input', onMemorySkipWIANInput);
1209 $('#summary_source').off('change').on('change', onSummarySourceChange);
1210 $('#memory_prompt_words').off('input').on('input', onMemoryPromptWordsInput);
1211 $('#memory_prompt_interval').off('input').on('input', onMemoryPromptIntervalInput);
1212 $('#memory_prompt').off('input').on('input', onMemoryPromptInput);
1213 $('#memory_force_summarize').off('click').on('click', () => forceSummarizeChat(false));
1214 $('#memory_template').off('input').on('input', onMemoryTemplateInput);
1215 $('#memory_depth').off('input').on('input', onMemoryDepthInput);
1216 $('#memory_role').off('input').on('input', onMemoryRoleInput);
1217 $('#memory_summary_prompt_role').off('input').on('input', onMemorySummaryPromptRoleInput);
1218 $('input[name="memory_position"]').off('change').on('change', onMemoryPositionChange);
1219 $('#memory_prompt_words_force').off('input').on('input', onMemoryPromptWordsForceInput);
1220 $('#memory_prompt_builder_default').off('input').on('input', onMemoryPromptBuilderInput);
1221 $('#memory_prompt_builder_raw_blocking').off('input').on('input', onMemoryPromptBuilderInput);
1222 $('#memory_prompt_builder_raw_non_blocking').off('input').on('input', onMemoryPromptBuilderInput);
1223 $('#memory_prompt_restore').off('click').on('click', onMemoryPromptRestoreClick);
1224 $('#memory_prompt_interval_auto').off('click').on('click', onPromptIntervalAutoClick);
1225 $('#memory_prompt_words_auto').off('click').on('click', onPromptForceWordsAutoClick);
1226 $('#memory_override_response_length').off('input').on('input', onOverrideResponseLengthInput);
1227 $('#memory_max_messages_per_request').off('input').on('input', onMaxMessagesPerRequestInput);
1228 $('#memory_include_wi_scan').off('input').on('input', onMemoryIncludeWIScanInput);
1229 $('#summarySettingsBlockToggle').off('click').on('click', function () {
1230 $('#summarySettingsBlock').slideToggle(200, 'swing');
1231 });
1232}
1233
1234export async function init() {
1235 async function addExtensionControls() {
1236 const settingsHtml = await renderExtensionTemplateAsync('memory', 'settings', { defaultSettings });
1237 $('#summarize_container').append(settingsHtml);
1238 setupListeners();
1239 $('#summaryExtensionPopoutButton').off('click').on('click', function (e) {
1240 doPopout(e);
1241 e.stopPropagation();
1242 });
1243 }
1244
1245 await addExtensionControls();
1246 loadSettings();
1247 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
1248 eventSource.makeLast(event_types.CHARACTER_MESSAGE_RENDERED, onChatEvent);
1249 for (const event of [event_types.MESSAGE_DELETED, event_types.MESSAGE_UPDATED, event_types.MESSAGE_SWIPED]) {
1250 eventSource.on(event, onChatEvent);
1251 }
1252 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1253 name: 'summarize',
1254 callback: summarizeCallback,
1255 namedArgumentList: [
1256 new SlashCommandNamedArgument('source', 'API to use for summarization', [ARGUMENT_TYPE.STRING], false, false, '', Object.values(summary_sources)),
1257 SlashCommandNamedArgument.fromProps({
1258 name: 'prompt',
1259 description: 'prompt to use for summarization',
1260 typeList: [ARGUMENT_TYPE.STRING],
1261 defaultValue: '',
1262 }),
1263 SlashCommandNamedArgument.fromProps({
1264 name: 'quiet',
1265 description: 'suppress the toast message when summarizing the chat',
1266 typeList: [ARGUMENT_TYPE.BOOLEAN],
1267 defaultValue: 'false',
1268 enumList: commonEnumProviders.boolean('trueFalse')(),
1269 }),
1270 ],
1271 unnamedArgumentList: [
1272 new SlashCommandArgument('text to summarize', [ARGUMENT_TYPE.STRING], false, false, ''),
1273 ],
1274 helpString: 'Summarizes the given text. If no text is provided, the current chat will be summarized. Can specify the source and the prompt to use.',
1275 returns: ARGUMENT_TYPE.STRING,
1276 }));
1277
1278 const summaryMacroHandler = () => {
1279 // Checking content of the UI summary box first
1280 const uiSummary = $('#memory_contents').val().toString();
1281 if (uiSummary.trim().length > 0) {
1282 return uiSummary;
1283 }
1284 // Fallback to scanning the chat for the latest summary if the UI summary box is empty
1285 return getLatestMemoryFromChat(getContext().chat);
1286 };
1287 if (power_user.experimental_macro_engine) {
1288 macros.register('summary', {
1289 category: MacroCategory.CHAT,
1290 description: 'Returns the latest memory/summary from the current chat.',
1291 handler: () => summaryMacroHandler(),
1292 });
1293 } else {
1294 // TODO: Remove this when the experimental macro engine is replacing the old macro engine
1295 MacrosParser.registerMacro('summary',
1296 () => summaryMacroHandler(),
1297 'Returns the latest memory/summary from the current chat.');
1298 }
1299}