Move itemized prompts into separate module (#4229) * Move itemized prompts into separate module * Update public/scripts/itemized-prompts.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update public/scripts/itemized-prompts.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

8a6e6503f50f71b8ecf17aa3b4b2f799af7b01a9

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
2 files changed, +367 -344Ignore whitespace
public/script.js+15 -344
@@ -4,9 +4,7 @@ import {
4 Fuse,4 Fuse,
5 DOMPurify,5 DOMPurify,
6 hljs,6 hljs,
7 localforage,
8 Handlebars,7 Handlebars,
9 DiffMatchPatch,
10 SVGInject,8 SVGInject,
11 Popper,9 Popper,
12 initLibraryShims,10 initLibraryShims,
@@ -297,6 +295,7 @@ import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, Prompt
297import { accountStorage } from './scripts/util/AccountStorage.js';295import { accountStorage } from './scripts/util/AccountStorage.js';
298import { initWelcomeScreen, openPermanentAssistantChat, openPermanentAssistantCard, getPermanentAssistantAvatar } from './scripts/welcome-screen.js';296import { initWelcomeScreen, openPermanentAssistantChat, openPermanentAssistantCard, getPermanentAssistantAvatar } from './scripts/welcome-screen.js';
299import { initDataMaid } from './scripts/data-maid.js';297import { initDataMaid } from './scripts/data-maid.js';
298import { clearItemizedPrompts, deleteItemizedPrompts, findItemizedPromptSet, initItemizedPrompts, itemizedParams, itemizedPrompts, loadItemizedPrompts, promptItemize, replaceItemizedPromptText, saveItemizedPrompts } from './scripts/itemized-prompts.js';
300299
301// API OBJECT FOR EXTERNAL WIRING300// API OBJECT FOR EXTERNAL WIRING
302globalThis.SillyTavern = {301globalThis.SillyTavern = {
@@ -304,7 +303,9 @@ globalThis.SillyTavern = {
304 getContext,303 getContext,
305};304};
306305
307//exporting functions and vars for mods306/**
307 * Exports that were once part of this module. For backwards compatibility.
308 */
308export {309export {
309 user_avatar,310 user_avatar,
310 setUserAvatar,311 setUserAvatar,
@@ -314,6 +315,15 @@ export {
314 isOdd,315 isOdd,
315 countOccurrences,316 countOccurrences,
316 renderTemplate,317 renderTemplate,
318 promptItemize,
319 itemizedPrompts,
320 saveItemizedPrompts,
321 loadItemizedPrompts,
322 itemizedParams,
323 clearItemizedPrompts,
324 replaceItemizedPromptText,
325 deleteItemizedPrompts,
326 findItemizedPromptSet,
317};327};
318328
319/**329/**
@@ -577,9 +587,6 @@ export let mesForShowdownParse; //intended to be used as a context to compare sh
577export let converter;587export let converter;
578588
579// array for prompt token calculations589// array for prompt token calculations
580console.debug('initializing Prompt Itemization Array on Startup');
581const promptStorage = localforage.createInstance({ name: 'SillyTavern_Prompts' });
582export let itemizedPrompts = [];
583590
584export const systemUserName = 'SillyTavern System';591export const systemUserName = 'SillyTavern System';
585export const neutralCharacterName = 'Assistant';592export const neutralCharacterName = 'Assistant';
@@ -928,8 +935,6 @@ var kobold_horde_model = '';
928935
929export let token;936export let token;
930937
931var PromptArrayItemForRawPromptDisplay;
932var priorPromptArrayItemForRawPromptDisplay;
933938
934/** The tag of the active character. (NOT the id) */939/** The tag of the active character. (NOT the id) */
935export let active_character = '';940export let active_character = '';
@@ -1038,6 +1043,7 @@ async function firstLoadInit() {
1038 await initScrapers();1043 await initScrapers();
1039 initCustomSelectedSamplers();1044 initCustomSelectedSamplers();
1040 initDataMaid();1045 initDataMaid();
1046 initItemizedPrompts();
1041 addDebugFunctions();1047 addDebugFunctions();
1042 doDailyExtensionUpdatesCheck();1048 doDailyExtensionUpdatesCheck();
1043 await hideLoader();1049 await hideLoader();
@@ -1102,92 +1108,6 @@ export function setActiveGroup(entityOrKey) {
1102 if (active_group) active_character = null;1108 if (active_group) active_character = null;
1103}1109}
11041110
1105/**
1106 * Gets the itemized prompts for a chat.
1107 * @param {string} chatId Chat ID to load
1108 */
1109export async function loadItemizedPrompts(chatId) {
1110 try {
1111 if (!chatId) {
1112 itemizedPrompts = [];
1113 return;
1114 }
1115
1116 itemizedPrompts = await promptStorage.getItem(chatId);
1117
1118 if (!itemizedPrompts) {
1119 itemizedPrompts = [];
1120 }
1121 } catch {
1122 console.log('Error loading itemized prompts for chat', chatId);
1123 itemizedPrompts = [];
1124 }
1125}
1126
1127/**
1128 * Saves the itemized prompts for a chat.
1129 * @param {string} chatId Chat ID to save itemized prompts for
1130 */
1131export async function saveItemizedPrompts(chatId) {
1132 try {
1133 if (!chatId) {
1134 return;
1135 }
1136
1137 await promptStorage.setItem(chatId, itemizedPrompts);
1138 } catch {
1139 console.log('Error saving itemized prompts for chat', chatId);
1140 }
1141}
1142
1143/**
1144 * Replaces the itemized prompt text for a message.
1145 * @param {number} mesId Message ID to get itemized prompt for
1146 * @param {string} promptText New raw prompt text
1147 * @returns
1148 */
1149export async function replaceItemizedPromptText(mesId, promptText) {
1150 if (!Array.isArray(itemizedPrompts)) {
1151 itemizedPrompts = [];
1152 }
1153
1154 const itemizedPrompt = itemizedPrompts.find(x => x.mesId === mesId);
1155
1156 if (!itemizedPrompt) {
1157 return;
1158 }
1159
1160 itemizedPrompt.rawPrompt = promptText;
1161}
1162
1163/**
1164 * Deletes the itemized prompts for a chat.
1165 * @param {string} chatId Chat ID to delete itemized prompts for
1166 */
1167export async function deleteItemizedPrompts(chatId) {
1168 try {
1169 if (!chatId) {
1170 return;
1171 }
1172
1173 await promptStorage.removeItem(chatId);
1174 } catch {
1175 console.log('Error deleting itemized prompts for chat', chatId);
1176 }
1177}
1178
1179/**
1180 * Empties the itemized prompts array and caches.
1181 */
1182export async function clearItemizedPrompts() {
1183 try {
1184 await promptStorage.clear();
1185 itemizedPrompts = [];
1186 } catch {
1187 console.log('Error clearing itemized prompts');
1188 }
1189}
1190
1191async function getStatusHorde() {1111async function getStatusHorde() {
1192 try {1112 try {
1193 const hordeStatus = await checkHordeStatus();1113 const hordeStatus = await checkHordeStatus();
@@ -2089,7 +2009,7 @@ export async function clearChat() {
2089 } else { console.debug('saw no avatars'); }2009 } else { console.debug('saw no avatars'); }
20902010
2091 await saveItemizedPrompts(getCurrentChatId());2011 await saveItemizedPrompts(getCurrentChatId());
2092 itemizedPrompts = [];2012 itemizedPrompts.length = 0;
2093}2013}
20942014
2095export async function deleteLastMessage() {2015export async function deleteLastMessage() {
@@ -5724,231 +5644,6 @@ async function duplicateCharacter() {
5724 return '';5644 return '';
5725}5645}
57265646
5727export async function itemizedParams(itemizedPrompts, thisPromptSet, incomingMesId) {
5728 const params = {
5729 charDescriptionTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].charDescription),
5730 charPersonalityTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].charPersonality),
5731 scenarioTextTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].scenarioText),
5732 userPersonaStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].userPersona),
5733 worldInfoStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].worldInfoString),
5734 allAnchorsTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].allAnchors),
5735 summarizeStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].summarizeString),
5736 authorsNoteStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].authorsNoteString),
5737 smartContextStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].smartContextString),
5738 beforeScenarioAnchorTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].beforeScenarioAnchor),
5739 afterScenarioAnchorTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].afterScenarioAnchor),
5740 zeroDepthAnchorTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].zeroDepthAnchor), // TODO: unused
5741 thisPrompt_padding: itemizedPrompts[thisPromptSet].padding,
5742 this_main_api: itemizedPrompts[thisPromptSet].main_api,
5743 chatInjects: await getTokenCountAsync(itemizedPrompts[thisPromptSet].chatInjects),
5744 chatVectorsStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].chatVectorsString),
5745 dataBankVectorsStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].dataBankVectorsString),
5746 modelUsed: chat[incomingMesId]?.extra?.model,
5747 apiUsed: chat[incomingMesId]?.extra?.api,
5748 presetName: itemizedPrompts[thisPromptSet].presetName || t`(Unknown)`,
5749 messagesCount: String(itemizedPrompts[thisPromptSet].messagesCount ?? ''),
5750 examplesCount: String(itemizedPrompts[thisPromptSet].examplesCount ?? ''),
5751 };
5752
5753 const getFriendlyName = (value) => $(`#rm_api_block select option[value="${value}"]`).first().text() || value;
5754
5755 if (params.apiUsed) {
5756 params.apiUsed = getFriendlyName(params.apiUsed);
5757 }
5758
5759 if (params.this_main_api) {
5760 params.mainApiFriendlyName = getFriendlyName(params.this_main_api);
5761 }
5762
5763 if (params.chatInjects) {
5764 params.ActualChatHistoryTokens = params.ActualChatHistoryTokens - params.chatInjects;
5765 }
5766
5767 if (params.this_main_api == 'openai') {
5768 //for OAI API
5769 //console.log('-- Counting OAI Tokens');
5770
5771 //params.finalPromptTokens = itemizedPrompts[thisPromptSet].oaiTotalTokens;
5772 params.oaiMainTokens = itemizedPrompts[thisPromptSet].oaiMainTokens;
5773 params.oaiStartTokens = itemizedPrompts[thisPromptSet].oaiStartTokens;
5774 params.ActualChatHistoryTokens = itemizedPrompts[thisPromptSet].oaiConversationTokens;
5775 params.examplesStringTokens = itemizedPrompts[thisPromptSet].oaiExamplesTokens;
5776 params.oaiPromptTokens = itemizedPrompts[thisPromptSet].oaiPromptTokens - (params.afterScenarioAnchorTokens + params.beforeScenarioAnchorTokens) + params.examplesStringTokens;
5777 params.oaiBiasTokens = itemizedPrompts[thisPromptSet].oaiBiasTokens;
5778 params.oaiJailbreakTokens = itemizedPrompts[thisPromptSet].oaiJailbreakTokens;
5779 params.oaiNudgeTokens = itemizedPrompts[thisPromptSet].oaiNudgeTokens;
5780 params.oaiImpersonateTokens = itemizedPrompts[thisPromptSet].oaiImpersonateTokens;
5781 params.oaiNsfwTokens = itemizedPrompts[thisPromptSet].oaiNsfwTokens;
5782 params.finalPromptTokens =
5783 params.oaiStartTokens +
5784 params.oaiPromptTokens +
5785 params.oaiMainTokens +
5786 params.oaiNsfwTokens +
5787 params.oaiBiasTokens +
5788 params.oaiImpersonateTokens +
5789 params.oaiJailbreakTokens +
5790 params.oaiNudgeTokens +
5791 params.ActualChatHistoryTokens +
5792 //charDescriptionTokens +
5793 //charPersonalityTokens +
5794 //allAnchorsTokens +
5795 params.worldInfoStringTokens +
5796 params.beforeScenarioAnchorTokens +
5797 params.afterScenarioAnchorTokens;
5798 // Max context size - max completion tokens
5799 params.thisPrompt_max_context = (oai_settings.openai_max_context - oai_settings.openai_max_tokens);
5800
5801 //console.log('-- applying % on OAI tokens');
5802 params.oaiStartTokensPercentage = ((params.oaiStartTokens / (params.finalPromptTokens)) * 100).toFixed(2);
5803 params.storyStringTokensPercentage = (((params.afterScenarioAnchorTokens + params.beforeScenarioAnchorTokens + params.oaiPromptTokens) / (params.finalPromptTokens)) * 100).toFixed(2);
5804 params.ActualChatHistoryTokensPercentage = ((params.ActualChatHistoryTokens / (params.finalPromptTokens)) * 100).toFixed(2);
5805 params.promptBiasTokensPercentage = ((params.oaiBiasTokens / (params.finalPromptTokens)) * 100).toFixed(2);
5806 params.worldInfoStringTokensPercentage = ((params.worldInfoStringTokens / (params.finalPromptTokens)) * 100).toFixed(2);
5807 params.allAnchorsTokensPercentage = ((params.allAnchorsTokens / (params.finalPromptTokens)) * 100).toFixed(2);
5808 params.selectedTokenizer = getFriendlyTokenizerName(params.this_main_api).tokenizerName;
5809 params.oaiSystemTokens = params.oaiImpersonateTokens + params.oaiJailbreakTokens + params.oaiNudgeTokens + params.oaiStartTokens + params.oaiNsfwTokens + params.oaiMainTokens;
5810 params.oaiSystemTokensPercentage = ((params.oaiSystemTokens / (params.finalPromptTokens)) * 100).toFixed(2);
5811 } else {
5812 //for non-OAI APIs
5813 //console.log('-- Counting non-OAI Tokens');
5814 params.finalPromptTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].finalPrompt);
5815 params.storyStringTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].storyString) - params.worldInfoStringTokens;
5816 params.examplesStringTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].examplesString);
5817 params.mesSendStringTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].mesSendString);
5818 params.ActualChatHistoryTokens = params.mesSendStringTokens - (params.allAnchorsTokens - (params.beforeScenarioAnchorTokens + params.afterScenarioAnchorTokens)) + power_user.token_padding;
5819 params.instructionTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].instruction);
5820 params.promptBiasTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].promptBias);
5821
5822 params.totalTokensInPrompt =
5823 params.storyStringTokens + //chardefs total
5824 params.worldInfoStringTokens +
5825 params.examplesStringTokens + // example messages
5826 params.ActualChatHistoryTokens + //chat history
5827 params.allAnchorsTokens + // AN and/or legacy anchors
5828 //afterScenarioAnchorTokens + //only counts if AN is set to 'after scenario'
5829 //zeroDepthAnchorTokens + //same as above, even if AN not on 0 depth
5830 params.promptBiasTokens; //{{}}
5831 //- thisPrompt_padding; //not sure this way of calculating is correct, but the math results in same value as 'finalPrompt'
5832 params.thisPrompt_max_context = itemizedPrompts[thisPromptSet].this_max_context;
5833 params.thisPrompt_actual = params.thisPrompt_max_context - params.thisPrompt_padding;
5834
5835 //console.log('-- applying % on non-OAI tokens');
5836 params.storyStringTokensPercentage = ((params.storyStringTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
5837 params.ActualChatHistoryTokensPercentage = ((params.ActualChatHistoryTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
5838 params.promptBiasTokensPercentage = ((params.promptBiasTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
5839 params.worldInfoStringTokensPercentage = ((params.worldInfoStringTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
5840 params.allAnchorsTokensPercentage = ((params.allAnchorsTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
5841 params.selectedTokenizer = itemizedPrompts[thisPromptSet]?.tokenizer || getFriendlyTokenizerName(params.this_main_api).tokenizerName;
5842 }
5843 return params;
5844}
5845
5846export function findItemizedPromptSet(itemizedPrompts, incomingMesId) {
5847 var thisPromptSet = undefined;
5848
5849 for (var i = 0; i < itemizedPrompts.length; i++) {
5850 console.log(`looking for ${incomingMesId} vs ${itemizedPrompts[i].mesId}`);
5851 if (itemizedPrompts[i].mesId === incomingMesId) {
5852 console.log(`found matching mesID ${i}`);
5853 thisPromptSet = i;
5854 PromptArrayItemForRawPromptDisplay = i;
5855 console.log(`wanting to raw display of ArrayItem: ${PromptArrayItemForRawPromptDisplay} which is mesID ${incomingMesId}`);
5856 console.log(itemizedPrompts[thisPromptSet]);
5857 break;
5858 } else if (itemizedPrompts[i].rawPrompt) {
5859 priorPromptArrayItemForRawPromptDisplay = i;
5860 }
5861 }
5862 return thisPromptSet;
5863}
5864
5865async function promptItemize(itemizedPrompts, requestedMesId) {
5866 console.log('PROMPT ITEMIZE ENTERED');
5867 var incomingMesId = Number(requestedMesId);
5868 console.debug(`looking for MesId ${incomingMesId}`);
5869 var thisPromptSet = findItemizedPromptSet(itemizedPrompts, incomingMesId);
5870
5871 if (thisPromptSet === undefined) {
5872 console.log(`couldnt find the right mesId. looked for ${incomingMesId}`);
5873 console.log(itemizedPrompts);
5874 return null;
5875 }
5876
5877 const params = await itemizedParams(itemizedPrompts, thisPromptSet, incomingMesId);
5878 const flatten = (rawPrompt) => Array.isArray(rawPrompt) ? rawPrompt.map(x => x.content).join('\n') : rawPrompt;
5879
5880 const template = params.this_main_api == 'openai'
5881 ? await renderTemplateAsync('itemizationChat', params)
5882 : await renderTemplateAsync('itemizationText', params);
5883
5884 const popup = new Popup(template, POPUP_TYPE.TEXT);
5885
5886 /** @type {HTMLElement} */
5887 const diffPrevPrompt = popup.dlg.querySelector('#diffPrevPrompt');
5888 if (priorPromptArrayItemForRawPromptDisplay) {
5889 diffPrevPrompt.style.display = '';
5890 diffPrevPrompt.addEventListener('click', function () {
5891 const dmp = new DiffMatchPatch();
5892 const text1 = flatten(itemizedPrompts[priorPromptArrayItemForRawPromptDisplay].rawPrompt);
5893 const text2 = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
5894
5895 dmp.Diff_Timeout = 2.0;
5896
5897 const d = dmp.diff_main(text1, text2);
5898 let ds = dmp.diff_prettyHtml(d);
5899 // make it readable
5900 ds = ds.replaceAll('background:#e6ffe6;', 'background:#b9f3b9; color:black;');
5901 ds = ds.replaceAll('background:#ffe6e6;', 'background:#f5b4b4; color:black;');
5902 ds = ds.replaceAll('&para;', '');
5903 const container = document.createElement('div');
5904 container.innerHTML = DOMPurify.sanitize(ds);
5905 const rawPromptWrapper = document.getElementById('rawPromptWrapper');
5906 rawPromptWrapper.replaceChildren(container);
5907 $('#rawPromptPopup').slideToggle();
5908 });
5909 } else {
5910 diffPrevPrompt.style.display = 'none';
5911 }
5912 popup.dlg.querySelector('#copyPromptToClipboard').addEventListener('pointerup', async function () {
5913 let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt;
5914 let rawPromptValues = rawPrompt;
5915
5916 if (Array.isArray(rawPrompt)) {
5917 rawPromptValues = rawPrompt.map(x => x.content).join('\n');
5918 }
5919
5920 await copyText(rawPromptValues);
5921 toastr.info(t`Copied!`);
5922 });
5923
5924 popup.dlg.querySelector('#showRawPrompt').addEventListener('click', async function () {
5925 //console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
5926 console.log(PromptArrayItemForRawPromptDisplay);
5927 console.log(itemizedPrompts);
5928 console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
5929
5930 const rawPrompt = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
5931
5932 // Mobile needs special handholding. The side-view on the popup wouldn't work,
5933 // so we just show an additional popup for this.
5934 if (isMobile()) {
5935 const content = document.createElement('div');
5936 content.classList.add('tokenItemizingMaintext');
5937 content.innerText = rawPrompt;
5938 const popup = new Popup(content, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true, leftAlign: true });
5939 await popup.show();
5940 return;
5941 }
5942
5943 //let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '<br>');
5944 const rawPromptWrapper = document.getElementById('rawPromptWrapper');
5945 rawPromptWrapper.innerText = rawPrompt;
5946 $('#rawPromptPopup').slideToggle();
5947 });
5948
5949 await popup.show();
5950}
5951
5952function setInContextMessages(msgInContextCount, type) {5647function setInContextMessages(msgInContextCount, type) {
5953 $('#chat .mes').removeClass('lastInContext');5648 $('#chat .mes').removeClass('lastInContext');
59545649
@@ -10474,15 +10169,6 @@ function addDebugFunctions() {
10474 const message = await generateRaw(text, null, false, false);10169 const message = await generateRaw(text, null, false, false);
10475 alert(message);10170 alert(message);
10476 });10171 });
10477
10478 registerDebugFunction('clearPrompts', 'Delete itemized prompts', 'Deletes all itemized prompts from the local storage.', async () => {
10479 await clearItemizedPrompts();
10480 toastr.info('Itemized prompts deleted.');
10481 if (getCurrentChatId()) {
10482 await reloadCurrentChat();
10483 }
10484 });
10485
10486 registerDebugFunction('toggleEventTracing', 'Toggle event tracing', 'Useful to see what triggered a certain event.', () => {10172 registerDebugFunction('toggleEventTracing', 'Toggle event tracing', 'Useful to see what triggered a certain event.', () => {
10487 localStorage.setItem('eventTracing', localStorage.getItem('eventTracing') === 'true' ? 'false' : 'true');10173 localStorage.setItem('eventTracing', localStorage.getItem('eventTracing') === 'true' ? 'false' : 'true');
10488 toastr.info('Event tracing is now ' + (localStorage.getItem('eventTracing') === 'true' ? 'enabled' : 'disabled'));10174 toastr.info('Event tracing is now ' + (localStorage.getItem('eventTracing') === 'true' ? 'enabled' : 'disabled'));
@@ -11614,14 +11300,6 @@ jQuery(async function () {
11614 }11300 }
11615 });11301 });
1161611302
11617 $(document).on('pointerup', '.mes_prompt', async function () {
11618 let mesIdForItemization = $(this).closest('.mes').attr('mesId');
11619 console.log(`looking for mesID: ${mesIdForItemization}`);
11620 if (itemizedPrompts.length !== undefined && itemizedPrompts.length !== 0) {
11621 await promptItemize(itemizedPrompts, mesIdForItemization);
11622 }
11623 });
11624
11625 //********************11303 //********************
11626 //***Message Editor***11304 //***Message Editor***
11627 $(document).on('click', '.mes_edit', async function () {11305 $(document).on('click', '.mes_edit', async function () {
@@ -12517,13 +12195,6 @@ jQuery(async function () {
12517 // Added here to prevent execution before script.js is loaded and get rid of quirky timeouts12195 // Added here to prevent execution before script.js is loaded and get rid of quirky timeouts
12518 await firstLoadInit();12196 await firstLoadInit();
1251912197
12520 eventSource.on(event_types.CHAT_DELETED, async (name) => {
12521 await deleteItemizedPrompts(name);
12522 });
12523 eventSource.on(event_types.GROUP_CHAT_DELETED, async (name) => {
12524 await deleteItemizedPrompts(name);
12525 });
12526
12527 window.addEventListener('beforeunload', (e) => {12198 window.addEventListener('beforeunload', (e) => {
12528 if (isChatSaving) {12199 if (isChatSaving) {
12529 e.preventDefault();12200 e.preventDefault();
public/scripts/itemized-prompts.js+352 -0
@@ -0,0 +1,352 @@
1import { DiffMatchPatch, DOMPurify, localforage } from '../lib.js';
2import { chat, event_types, eventSource, getCurrentChatId, reloadCurrentChat } from '../script.js';
3import { t } from './i18n.js';
4import { oai_settings } from './openai.js';
5import { Popup, POPUP_TYPE } from './popup.js';
6import { power_user, registerDebugFunction } from './power-user.js';
7import { isMobile } from './RossAscends-mods.js';
8import { renderTemplateAsync } from './templates.js';
9import { getFriendlyTokenizerName, getTokenCountAsync } from './tokenizers.js';
10import { copyText } from './utils.js';
11
12let PromptArrayItemForRawPromptDisplay;
13let priorPromptArrayItemForRawPromptDisplay;
14
15const promptStorage = localforage.createInstance({ name: 'SillyTavern_Prompts' });
16export let itemizedPrompts = [];
17
18/**
19 * Gets the itemized prompts for a chat.
20 * @param {string} chatId Chat ID to load
21 */
22export async function loadItemizedPrompts(chatId) {
23 try {
24 if (!chatId) {
25 itemizedPrompts = [];
26 return;
27 }
28
29 itemizedPrompts = await promptStorage.getItem(chatId);
30
31 if (!itemizedPrompts) {
32 itemizedPrompts = [];
33 }
34 } catch {
35 console.log('Error loading itemized prompts for chat', chatId);
36 itemizedPrompts = [];
37 }
38}
39
40/**
41 * Saves the itemized prompts for a chat.
42 * @param {string} chatId Chat ID to save itemized prompts for
43 */
44export async function saveItemizedPrompts(chatId) {
45 try {
46 if (!chatId) {
47 return;
48 }
49
50 await promptStorage.setItem(chatId, itemizedPrompts);
51 } catch {
52 console.log('Error saving itemized prompts for chat', chatId);
53 }
54}
55
56/**
57 * Replaces the itemized prompt text for a message.
58 * @param {number} mesId Message ID to get itemized prompt for
59 * @param {string} promptText New raw prompt text
60 * @returns
61 */
62export async function replaceItemizedPromptText(mesId, promptText) {
63 if (!Array.isArray(itemizedPrompts)) {
64 itemizedPrompts = [];
65 }
66
67 const itemizedPrompt = itemizedPrompts.find(x => x.mesId === mesId);
68
69 if (!itemizedPrompt) {
70 return;
71 }
72
73 itemizedPrompt.rawPrompt = promptText;
74}
75
76/**
77 * Deletes the itemized prompts for a chat.
78 * @param {string} chatId Chat ID to delete itemized prompts for
79 */
80export async function deleteItemizedPrompts(chatId) {
81 try {
82 if (!chatId) {
83 return;
84 }
85
86 await promptStorage.removeItem(chatId);
87 } catch {
88 console.log('Error deleting itemized prompts for chat', chatId);
89 }
90}
91
92/**
93 * Empties the itemized prompts array and caches.
94 */
95export async function clearItemizedPrompts() {
96 try {
97 await promptStorage.clear();
98 itemizedPrompts = [];
99 } catch {
100 console.log('Error clearing itemized prompts');
101 }
102}
103
104export async function itemizedParams(itemizedPrompts, thisPromptSet, incomingMesId) {
105 const params = {
106 charDescriptionTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].charDescription),
107 charPersonalityTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].charPersonality),
108 scenarioTextTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].scenarioText),
109 userPersonaStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].userPersona),
110 worldInfoStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].worldInfoString),
111 allAnchorsTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].allAnchors),
112 summarizeStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].summarizeString),
113 authorsNoteStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].authorsNoteString),
114 smartContextStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].smartContextString),
115 beforeScenarioAnchorTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].beforeScenarioAnchor),
116 afterScenarioAnchorTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].afterScenarioAnchor),
117 zeroDepthAnchorTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].zeroDepthAnchor), // TODO: unused
118 thisPrompt_padding: itemizedPrompts[thisPromptSet].padding,
119 this_main_api: itemizedPrompts[thisPromptSet].main_api,
120 chatInjects: await getTokenCountAsync(itemizedPrompts[thisPromptSet].chatInjects),
121 chatVectorsStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].chatVectorsString),
122 dataBankVectorsStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].dataBankVectorsString),
123 modelUsed: chat[incomingMesId]?.extra?.model,
124 apiUsed: chat[incomingMesId]?.extra?.api,
125 presetName: itemizedPrompts[thisPromptSet].presetName || t`(Unknown)`,
126 messagesCount: String(itemizedPrompts[thisPromptSet].messagesCount ?? ''),
127 examplesCount: String(itemizedPrompts[thisPromptSet].examplesCount ?? ''),
128 };
129
130 const getFriendlyName = (value) => $(`#rm_api_block select option[value="${value}"]`).first().text() || value;
131
132 if (params.apiUsed) {
133 params.apiUsed = getFriendlyName(params.apiUsed);
134 }
135
136 if (params.this_main_api) {
137 params.mainApiFriendlyName = getFriendlyName(params.this_main_api);
138 }
139
140 if (params.chatInjects) {
141 params.ActualChatHistoryTokens = params.ActualChatHistoryTokens - params.chatInjects;
142 }
143
144 if (params.this_main_api == 'openai') {
145 //for OAI API
146 //console.log('-- Counting OAI Tokens');
147
148 //params.finalPromptTokens = itemizedPrompts[thisPromptSet].oaiTotalTokens;
149 params.oaiMainTokens = itemizedPrompts[thisPromptSet].oaiMainTokens;
150 params.oaiStartTokens = itemizedPrompts[thisPromptSet].oaiStartTokens;
151 params.ActualChatHistoryTokens = itemizedPrompts[thisPromptSet].oaiConversationTokens;
152 params.examplesStringTokens = itemizedPrompts[thisPromptSet].oaiExamplesTokens;
153 params.oaiPromptTokens = itemizedPrompts[thisPromptSet].oaiPromptTokens - (params.afterScenarioAnchorTokens + params.beforeScenarioAnchorTokens) + params.examplesStringTokens;
154 params.oaiBiasTokens = itemizedPrompts[thisPromptSet].oaiBiasTokens;
155 params.oaiJailbreakTokens = itemizedPrompts[thisPromptSet].oaiJailbreakTokens;
156 params.oaiNudgeTokens = itemizedPrompts[thisPromptSet].oaiNudgeTokens;
157 params.oaiImpersonateTokens = itemizedPrompts[thisPromptSet].oaiImpersonateTokens;
158 params.oaiNsfwTokens = itemizedPrompts[thisPromptSet].oaiNsfwTokens;
159 params.finalPromptTokens =
160 params.oaiStartTokens +
161 params.oaiPromptTokens +
162 params.oaiMainTokens +
163 params.oaiNsfwTokens +
164 params.oaiBiasTokens +
165 params.oaiImpersonateTokens +
166 params.oaiJailbreakTokens +
167 params.oaiNudgeTokens +
168 params.ActualChatHistoryTokens +
169 //charDescriptionTokens +
170 //charPersonalityTokens +
171 //allAnchorsTokens +
172 params.worldInfoStringTokens +
173 params.beforeScenarioAnchorTokens +
174 params.afterScenarioAnchorTokens;
175 // Max context size - max completion tokens
176 params.thisPrompt_max_context = (oai_settings.openai_max_context - oai_settings.openai_max_tokens);
177
178 //console.log('-- applying % on OAI tokens');
179 params.oaiStartTokensPercentage = ((params.oaiStartTokens / (params.finalPromptTokens)) * 100).toFixed(2);
180 params.storyStringTokensPercentage = (((params.afterScenarioAnchorTokens + params.beforeScenarioAnchorTokens + params.oaiPromptTokens) / (params.finalPromptTokens)) * 100).toFixed(2);
181 params.ActualChatHistoryTokensPercentage = ((params.ActualChatHistoryTokens / (params.finalPromptTokens)) * 100).toFixed(2);
182 params.promptBiasTokensPercentage = ((params.oaiBiasTokens / (params.finalPromptTokens)) * 100).toFixed(2);
183 params.worldInfoStringTokensPercentage = ((params.worldInfoStringTokens / (params.finalPromptTokens)) * 100).toFixed(2);
184 params.allAnchorsTokensPercentage = ((params.allAnchorsTokens / (params.finalPromptTokens)) * 100).toFixed(2);
185 params.selectedTokenizer = getFriendlyTokenizerName(params.this_main_api).tokenizerName;
186 params.oaiSystemTokens = params.oaiImpersonateTokens + params.oaiJailbreakTokens + params.oaiNudgeTokens + params.oaiStartTokens + params.oaiNsfwTokens + params.oaiMainTokens;
187 params.oaiSystemTokensPercentage = ((params.oaiSystemTokens / (params.finalPromptTokens)) * 100).toFixed(2);
188 } else {
189 //for non-OAI APIs
190 //console.log('-- Counting non-OAI Tokens');
191 params.finalPromptTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].finalPrompt);
192 params.storyStringTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].storyString) - params.worldInfoStringTokens;
193 params.examplesStringTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].examplesString);
194 params.mesSendStringTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].mesSendString);
195 params.ActualChatHistoryTokens = params.mesSendStringTokens - (params.allAnchorsTokens - (params.beforeScenarioAnchorTokens + params.afterScenarioAnchorTokens)) + power_user.token_padding;
196 params.instructionTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].instruction);
197 params.promptBiasTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].promptBias);
198
199 params.totalTokensInPrompt =
200 params.storyStringTokens + //chardefs total
201 params.worldInfoStringTokens +
202 params.examplesStringTokens + // example messages
203 params.ActualChatHistoryTokens + //chat history
204 params.allAnchorsTokens + // AN and/or legacy anchors
205 //afterScenarioAnchorTokens + //only counts if AN is set to 'after scenario'
206 //zeroDepthAnchorTokens + //same as above, even if AN not on 0 depth
207 params.promptBiasTokens; //{{}}
208 //- thisPrompt_padding; //not sure this way of calculating is correct, but the math results in same value as 'finalPrompt'
209 params.thisPrompt_max_context = itemizedPrompts[thisPromptSet].this_max_context;
210 params.thisPrompt_actual = params.thisPrompt_max_context - params.thisPrompt_padding;
211
212 //console.log('-- applying % on non-OAI tokens');
213 params.storyStringTokensPercentage = ((params.storyStringTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
214 params.ActualChatHistoryTokensPercentage = ((params.ActualChatHistoryTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
215 params.promptBiasTokensPercentage = ((params.promptBiasTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
216 params.worldInfoStringTokensPercentage = ((params.worldInfoStringTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
217 params.allAnchorsTokensPercentage = ((params.allAnchorsTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
218 params.selectedTokenizer = itemizedPrompts[thisPromptSet]?.tokenizer || getFriendlyTokenizerName(params.this_main_api).tokenizerName;
219 }
220 return params;
221}
222
223export function findItemizedPromptSet(itemizedPrompts, incomingMesId) {
224 let thisPromptSet = undefined;
225
226 for (let i = 0; i < itemizedPrompts.length; i++) {
227 console.log(`looking for ${incomingMesId} vs ${itemizedPrompts[i].mesId}`);
228 if (itemizedPrompts[i].mesId === incomingMesId) {
229 console.log(`found matching mesID ${i}`);
230 thisPromptSet = i;
231 PromptArrayItemForRawPromptDisplay = i;
232 console.log(`wanting to raw display of ArrayItem: ${PromptArrayItemForRawPromptDisplay} which is mesID ${incomingMesId}`);
233 console.log(itemizedPrompts[thisPromptSet]);
234 break;
235 } else if (itemizedPrompts[i].rawPrompt) {
236 priorPromptArrayItemForRawPromptDisplay = i;
237 }
238 }
239 return thisPromptSet;
240}
241
242export async function promptItemize(itemizedPrompts, requestedMesId) {
243 console.log('PROMPT ITEMIZE ENTERED');
244 var incomingMesId = Number(requestedMesId);
245 console.debug(`looking for MesId ${incomingMesId}`);
246 var thisPromptSet = findItemizedPromptSet(itemizedPrompts, incomingMesId);
247
248 if (thisPromptSet === undefined) {
249 console.log(`couldnt find the right mesId. looked for ${incomingMesId}`);
250 console.log(itemizedPrompts);
251 return null;
252 }
253
254 const params = await itemizedParams(itemizedPrompts, thisPromptSet, incomingMesId);
255 const flatten = (rawPrompt) => Array.isArray(rawPrompt) ? rawPrompt.map(x => x.content).join('\n') : rawPrompt;
256
257 const template = params.this_main_api == 'openai'
258 ? await renderTemplateAsync('itemizationChat', params)
259 : await renderTemplateAsync('itemizationText', params);
260
261 const popup = new Popup(template, POPUP_TYPE.TEXT);
262
263 /** @type {HTMLElement} */
264 const diffPrevPrompt = popup.dlg.querySelector('#diffPrevPrompt');
265 if (priorPromptArrayItemForRawPromptDisplay) {
266 diffPrevPrompt.style.display = '';
267 diffPrevPrompt.addEventListener('click', function () {
268 const dmp = new DiffMatchPatch();
269 const text1 = flatten(itemizedPrompts[priorPromptArrayItemForRawPromptDisplay].rawPrompt);
270 const text2 = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
271
272 dmp.Diff_Timeout = 2.0;
273
274 const d = dmp.diff_main(text1, text2);
275 let ds = dmp.diff_prettyHtml(d);
276 // make it readable
277 ds = ds.replaceAll('background:#e6ffe6;', 'background:#b9f3b9; color:black;');
278 ds = ds.replaceAll('background:#ffe6e6;', 'background:#f5b4b4; color:black;');
279 ds = ds.replaceAll('&para;', '');
280 const container = document.createElement('div');
281 container.innerHTML = DOMPurify.sanitize(ds);
282 const rawPromptWrapper = document.getElementById('rawPromptWrapper');
283 rawPromptWrapper.replaceChildren(container);
284 $('#rawPromptPopup').slideToggle();
285 });
286 } else {
287 diffPrevPrompt.style.display = 'none';
288 }
289 popup.dlg.querySelector('#copyPromptToClipboard').addEventListener('pointerup', async function () {
290 let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt;
291 let rawPromptValues = rawPrompt;
292
293 if (Array.isArray(rawPrompt)) {
294 rawPromptValues = rawPrompt.map(x => x.content).join('\n');
295 }
296
297 await copyText(rawPromptValues);
298 toastr.info(t`Copied!`);
299 });
300
301 popup.dlg.querySelector('#showRawPrompt').addEventListener('click', async function () {
302 //console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
303 console.log(PromptArrayItemForRawPromptDisplay);
304 console.log(itemizedPrompts);
305 console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
306
307 const rawPrompt = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
308
309 // Mobile needs special handholding. The side-view on the popup wouldn't work,
310 // so we just show an additional popup for this.
311 if (isMobile()) {
312 const content = document.createElement('div');
313 content.classList.add('tokenItemizingMaintext');
314 content.innerText = rawPrompt;
315 const popup = new Popup(content, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true, leftAlign: true });
316 await popup.show();
317 return;
318 }
319
320 //let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '<br>');
321 const rawPromptWrapper = document.getElementById('rawPromptWrapper');
322 rawPromptWrapper.innerText = rawPrompt;
323 $('#rawPromptPopup').slideToggle();
324 });
325
326 await popup.show();
327}
328
329export function initItemizedPrompts() {
330 registerDebugFunction('clearPrompts', 'Delete itemized prompts', 'Deletes all itemized prompts from the local storage.', async () => {
331 await clearItemizedPrompts();
332 toastr.info('Itemized prompts deleted.');
333 if (getCurrentChatId()) {
334 await reloadCurrentChat();
335 }
336 });
337
338 $(document).on('pointerup', '.mes_prompt', async function () {
339 let mesIdForItemization = $(this).closest('.mes').attr('mesId');
340 console.log(`looking for mesID: ${mesIdForItemization}`);
341 if (itemizedPrompts.length !== undefined && itemizedPrompts.length !== 0) {
342 await promptItemize(itemizedPrompts, mesIdForItemization);
343 }
344 });
345
346 eventSource.on(event_types.CHAT_DELETED, async (name) => {
347 await deleteItemizedPrompts(name);
348 });
349 eventSource.on(event_types.GROUP_CHAT_DELETED, async (name) => {
350 await deleteItemizedPrompts(name);
351 });
352}