Blame Raw
Cohee · 51ad27fb · · 399 lines (18.3 KB)
2 contributors
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
35 await eventSource.emit(event_types.ITEMIZED_PROMPTS_LOADED, { chatId: chatId });
36 } catch {
37 console.log('Error loading itemized prompts for chat', chatId);
38 itemizedPrompts = [];
39 }
40}
41
42/**
43 * Saves the itemized prompts for a chat.
44 * @param {string} chatId Chat ID to save itemized prompts for
45 */
46export async function saveItemizedPrompts(chatId) {
47 try {
48 if (!chatId) {
49 return;
50 }
51
52 await promptStorage.setItem(chatId, itemizedPrompts);
53 await eventSource.emit(event_types.ITEMIZED_PROMPTS_SAVED, { chatId: chatId });
54 } catch {
55 console.log('Error saving itemized prompts for chat', chatId);
56 }
57}
58
59/**
60 * Replaces the itemized prompt text for a message.
61 * @param {number} mesId Message ID to get itemized prompt for
62 * @param {string} promptText New raw prompt text
63 * @returns
64 */
65export async function replaceItemizedPromptText(mesId, promptText) {
66 if (!Array.isArray(itemizedPrompts)) {
67 itemizedPrompts = [];
68 }
69
70 const itemizedPrompt = itemizedPrompts.find(x => x.mesId === mesId);
71
72 if (!itemizedPrompt) {
73 return;
74 }
75
76 itemizedPrompt.rawPrompt = promptText;
77}
78
79/**
80 * Deletes the itemized prompts for a chat.
81 * @param {string} chatId Chat ID to delete itemized prompts for
82 */
83export async function deleteItemizedPrompts(chatId) {
84 try {
85 if (!chatId) {
86 return;
87 }
88
89 await promptStorage.removeItem(chatId);
90 await eventSource.emit(event_types.ITEMIZED_PROMPTS_DELETED, { chatId: chatId, all: false });
91 } catch {
92 console.log('Error deleting itemized prompts for chat', chatId);
93 }
94}
95
96/**
97 * Empties the itemized prompts array and caches.
98 */
99export async function clearItemizedPrompts() {
100 try {
101 await promptStorage.clear();
102 itemizedPrompts = [];
103 await eventSource.emit(event_types.ITEMIZED_PROMPTS_DELETED, { all: true });
104 } catch {
105 console.log('Error clearing itemized prompts');
106 }
107}
108
109export async function itemizedParams(itemizedPrompts, thisPromptSet, incomingMesId) {
110 const params = {
111 charDescriptionTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].charDescription),
112 charPersonalityTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].charPersonality),
113 scenarioTextTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].scenarioText),
114 userPersonaStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].userPersona),
115 worldInfoStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].worldInfoString),
116 allAnchorsTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].allAnchors),
117 summarizeStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].summarizeString),
118 authorsNoteStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].authorsNoteString),
119 smartContextStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].smartContextString),
120 beforeScenarioAnchorTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].beforeScenarioAnchor),
121 afterScenarioAnchorTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].afterScenarioAnchor),
122 zeroDepthAnchorTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].zeroDepthAnchor), // TODO: unused
123 thisPrompt_padding: itemizedPrompts[thisPromptSet].padding,
124 this_main_api: itemizedPrompts[thisPromptSet].main_api,
125 chatInjects: await getTokenCountAsync(itemizedPrompts[thisPromptSet].chatInjects),
126 chatVectorsStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].chatVectorsString),
127 dataBankVectorsStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].dataBankVectorsString),
128 modelUsed: chat[incomingMesId]?.extra?.model,
129 apiUsed: chat[incomingMesId]?.extra?.api,
130 presetName: itemizedPrompts[thisPromptSet].presetName || t`(Unknown)`,
131 messagesCount: String(itemizedPrompts[thisPromptSet].messagesCount ?? ''),
132 examplesCount: String(itemizedPrompts[thisPromptSet].examplesCount ?? ''),
133 };
134
135 const getFriendlyName = (value) => $(`#rm_api_block select option[value="${value}"]`).first().text() || value;
136
137 if (params.apiUsed) {
138 params.apiUsed = getFriendlyName(params.apiUsed);
139 }
140
141 if (params.this_main_api) {
142 params.mainApiFriendlyName = getFriendlyName(params.this_main_api);
143 }
144
145 if (params.chatInjects) {
146 params.ActualChatHistoryTokens = params.ActualChatHistoryTokens - params.chatInjects;
147 }
148
149 if (params.this_main_api == 'openai') {
150 //for OAI API
151 //console.log('-- Counting OAI Tokens');
152
153 //params.finalPromptTokens = itemizedPrompts[thisPromptSet].oaiTotalTokens;
154 params.oaiMainTokens = itemizedPrompts[thisPromptSet].oaiMainTokens;
155 params.oaiStartTokens = itemizedPrompts[thisPromptSet].oaiStartTokens;
156 params.ActualChatHistoryTokens = itemizedPrompts[thisPromptSet].oaiConversationTokens;
157 params.examplesStringTokens = itemizedPrompts[thisPromptSet].oaiExamplesTokens;
158 params.oaiPromptTokens = itemizedPrompts[thisPromptSet].oaiPromptTokens - (params.afterScenarioAnchorTokens + params.beforeScenarioAnchorTokens) + params.examplesStringTokens;
159 params.oaiBiasTokens = itemizedPrompts[thisPromptSet].oaiBiasTokens;
160 params.oaiJailbreakTokens = itemizedPrompts[thisPromptSet].oaiJailbreakTokens;
161 params.oaiNudgeTokens = itemizedPrompts[thisPromptSet].oaiNudgeTokens;
162 params.oaiImpersonateTokens = itemizedPrompts[thisPromptSet].oaiImpersonateTokens;
163 params.oaiNsfwTokens = itemizedPrompts[thisPromptSet].oaiNsfwTokens;
164 params.finalPromptTokens =
165 params.oaiStartTokens +
166 params.oaiPromptTokens +
167 params.oaiMainTokens +
168 params.oaiNsfwTokens +
169 params.oaiBiasTokens +
170 params.oaiImpersonateTokens +
171 params.oaiJailbreakTokens +
172 params.oaiNudgeTokens +
173 params.ActualChatHistoryTokens +
174 //charDescriptionTokens +
175 //charPersonalityTokens +
176 //allAnchorsTokens +
177 params.worldInfoStringTokens +
178 params.beforeScenarioAnchorTokens +
179 params.afterScenarioAnchorTokens;
180 // Max context size - max completion tokens
181 params.thisPrompt_max_context = (oai_settings.openai_max_context - oai_settings.openai_max_tokens);
182
183 //console.log('-- applying % on OAI tokens');
184 params.oaiStartTokensPercentage = ((params.oaiStartTokens / (params.finalPromptTokens)) * 100).toFixed(2);
185 params.storyStringTokensPercentage = (((params.afterScenarioAnchorTokens + params.beforeScenarioAnchorTokens + params.oaiPromptTokens) / (params.finalPromptTokens)) * 100).toFixed(2);
186 params.ActualChatHistoryTokensPercentage = ((params.ActualChatHistoryTokens / (params.finalPromptTokens)) * 100).toFixed(2);
187 params.promptBiasTokensPercentage = ((params.oaiBiasTokens / (params.finalPromptTokens)) * 100).toFixed(2);
188 params.worldInfoStringTokensPercentage = ((params.worldInfoStringTokens / (params.finalPromptTokens)) * 100).toFixed(2);
189 params.allAnchorsTokensPercentage = ((params.allAnchorsTokens / (params.finalPromptTokens)) * 100).toFixed(2);
190 params.selectedTokenizer = getFriendlyTokenizerName(params.this_main_api).tokenizerName;
191 params.oaiSystemTokens = params.oaiImpersonateTokens + params.oaiJailbreakTokens + params.oaiNudgeTokens + params.oaiStartTokens + params.oaiNsfwTokens + params.oaiMainTokens;
192 params.oaiSystemTokensPercentage = ((params.oaiSystemTokens / (params.finalPromptTokens)) * 100).toFixed(2);
193 } else {
194 //for non-OAI APIs
195 //console.log('-- Counting non-OAI Tokens');
196 params.finalPromptTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].finalPrompt);
197 params.storyStringTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].storyString) - params.worldInfoStringTokens;
198 params.examplesStringTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].examplesString);
199 params.mesSendStringTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].mesSendString);
200 params.ActualChatHistoryTokens = params.mesSendStringTokens - (params.allAnchorsTokens - (params.beforeScenarioAnchorTokens + params.afterScenarioAnchorTokens)) + power_user.token_padding;
201 params.instructionTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].instruction);
202 params.promptBiasTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].promptBias);
203
204 params.totalTokensInPrompt =
205 params.storyStringTokens + //chardefs total
206 params.worldInfoStringTokens +
207 params.examplesStringTokens + // example messages
208 params.ActualChatHistoryTokens + //chat history
209 params.allAnchorsTokens + // AN and/or legacy anchors
210 //afterScenarioAnchorTokens + //only counts if AN is set to 'after scenario'
211 //zeroDepthAnchorTokens + //same as above, even if AN not on 0 depth
212 params.promptBiasTokens; //{{}}
213 //- thisPrompt_padding; //not sure this way of calculating is correct, but the math results in same value as 'finalPrompt'
214 params.thisPrompt_max_context = itemizedPrompts[thisPromptSet].this_max_context;
215 params.thisPrompt_actual = params.thisPrompt_max_context - params.thisPrompt_padding;
216
217 //console.log('-- applying % on non-OAI tokens');
218 params.storyStringTokensPercentage = ((params.storyStringTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
219 params.ActualChatHistoryTokensPercentage = ((params.ActualChatHistoryTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
220 params.promptBiasTokensPercentage = ((params.promptBiasTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
221 params.worldInfoStringTokensPercentage = ((params.worldInfoStringTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
222 params.allAnchorsTokensPercentage = ((params.allAnchorsTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
223 params.selectedTokenizer = itemizedPrompts[thisPromptSet]?.tokenizer || getFriendlyTokenizerName(params.this_main_api).tokenizerName;
224 }
225 return params;
226}
227
228export function findItemizedPromptSet(itemizedPrompts, incomingMesId) {
229 let thisPromptSet = undefined;
230 priorPromptArrayItemForRawPromptDisplay = -1;
231
232 for (let i = 0; i < itemizedPrompts.length; i++) {
233 console.log(`looking for ${incomingMesId} vs ${itemizedPrompts[i].mesId}`);
234 if (itemizedPrompts[i].mesId === incomingMesId) {
235 console.log(`found matching mesID ${i}`);
236 thisPromptSet = i;
237 PromptArrayItemForRawPromptDisplay = i;
238 console.log(`wanting to raw display of ArrayItem: ${PromptArrayItemForRawPromptDisplay} which is mesID ${incomingMesId}`);
239 console.log(itemizedPrompts[thisPromptSet]);
240 break;
241 } else if (itemizedPrompts[i].rawPrompt) {
242 priorPromptArrayItemForRawPromptDisplay = i;
243 }
244 }
245 return thisPromptSet;
246}
247
248export async function promptItemize(itemizedPrompts, requestedMesId) {
249 console.log('PROMPT ITEMIZE ENTERED');
250 var incomingMesId = Number(requestedMesId);
251 console.debug(`looking for MesId ${incomingMesId}`);
252 var thisPromptSet = findItemizedPromptSet(itemizedPrompts, incomingMesId);
253
254 if (thisPromptSet === undefined) {
255 console.log(`couldnt find the right mesId. looked for ${incomingMesId}`);
256 console.log(itemizedPrompts);
257 return null;
258 }
259
260 const params = await itemizedParams(itemizedPrompts, thisPromptSet, incomingMesId);
261 const flatten = (rawPrompt) => Array.isArray(rawPrompt) ? rawPrompt.map(x => x.content).join('\n') : rawPrompt;
262
263 const template = params.this_main_api == 'openai'
264 ? await renderTemplateAsync('itemizationChat', params)
265 : await renderTemplateAsync('itemizationText', params);
266
267 const popup = new Popup(template, POPUP_TYPE.TEXT);
268
269 /** @type {HTMLElement} */
270 const diffPrevPrompt = popup.dlg.querySelector('#diffPrevPrompt');
271 if (priorPromptArrayItemForRawPromptDisplay >= 0) {
272 diffPrevPrompt.style.display = '';
273 diffPrevPrompt.addEventListener('click', function () {
274 const dmp = new DiffMatchPatch();
275 const text1 = flatten(itemizedPrompts[priorPromptArrayItemForRawPromptDisplay].rawPrompt);
276 const text2 = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
277
278 dmp.Diff_Timeout = 2.0;
279
280 const d = dmp.diff_main(text1, text2);
281 let ds = dmp.diff_prettyHtml(d);
282 // make it readable
283 ds = ds.replaceAll('background:#e6ffe6;', 'background:#b9f3b9; color:black;');
284 ds = ds.replaceAll('background:#ffe6e6;', 'background:#f5b4b4; color:black;');
285 ds = ds.replaceAll('&para;', '');
286 const container = document.createElement('div');
287 container.innerHTML = DOMPurify.sanitize(ds);
288 const rawPromptWrapper = document.getElementById('rawPromptWrapper');
289 rawPromptWrapper.replaceChildren(container);
290 $('#rawPromptPopup').slideToggle();
291 });
292 } else {
293 diffPrevPrompt.style.display = 'none';
294 }
295 popup.dlg.querySelector('#copyPromptToClipboard').addEventListener('pointerup', async function () {
296 let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt;
297 let rawPromptValues = rawPrompt;
298
299 if (Array.isArray(rawPrompt)) {
300 rawPromptValues = rawPrompt.map(x => x.content).join('\n');
301 }
302
303 await copyText(rawPromptValues);
304 toastr.info(t`Copied!`);
305 });
306
307 popup.dlg.querySelector('#showRawPrompt').addEventListener('click', async function () {
308 //console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
309 console.log(PromptArrayItemForRawPromptDisplay);
310 console.log(itemizedPrompts);
311 console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
312
313 const rawPrompt = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
314
315 // Mobile needs special handholding. The side-view on the popup wouldn't work,
316 // so we just show an additional popup for this.
317 if (isMobile()) {
318 const content = document.createElement('div');
319 content.classList.add('tokenItemizingMaintext');
320 content.innerText = rawPrompt;
321 const popup = new Popup(content, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true, leftAlign: true });
322 await popup.show();
323 return;
324 }
325
326 //let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '<br>');
327 const rawPromptWrapper = document.getElementById('rawPromptWrapper');
328 rawPromptWrapper.innerText = rawPrompt;
329 $('#rawPromptPopup').slideToggle();
330 });
331
332 await popup.show();
333}
334
335export function initItemizedPrompts() {
336 registerDebugFunction('clearPrompts', 'Delete itemized prompts', 'Deletes all itemized prompts from the local storage.', async () => {
337 await clearItemizedPrompts();
338 toastr.info('Itemized prompts deleted.');
339 if (getCurrentChatId()) {
340 await reloadCurrentChat();
341 }
342 });
343
344 $(document).on('pointerup', '.mes_prompt', async function () {
345 let mesIdForItemization = $(this).closest('.mes').attr('mesId');
346 console.log(`looking for mesID: ${mesIdForItemization}`);
347 if (itemizedPrompts.length !== undefined && itemizedPrompts.length !== 0) {
348 await promptItemize(itemizedPrompts, mesIdForItemization);
349 }
350 });
351
352 eventSource.on(event_types.CHAT_DELETED, async (name) => {
353 await deleteItemizedPrompts(name);
354 });
355 eventSource.on(event_types.GROUP_CHAT_DELETED, async (name) => {
356 await deleteItemizedPrompts(name);
357 });
358}
359
360/**
361 * Swaps the itemized prompts between two messages. Useful when moving messages around in the chat.
362 * @param {number} sourceMessageId Source message ID
363 * @param {number} targetMessageId Target message ID
364 */
365export function swapItemizedPrompts(sourceMessageId, targetMessageId) {
366 if (!Array.isArray(itemizedPrompts)) {
367 return;
368 }
369
370 const sourcePrompts = itemizedPrompts.filter(x => x.mesId === sourceMessageId);
371 const targetPrompts = itemizedPrompts.filter(x => x.mesId === targetMessageId);
372
373 sourcePrompts.forEach(prompt => {
374 prompt.mesId = targetMessageId;
375 });
376
377 targetPrompts.forEach(prompt => {
378 prompt.mesId = sourceMessageId;
379 });
380
381 itemizedPrompts.sort((a, b) => a.mesId - b.mesId);
382}
383
384/**
385 * Deletes the itemized prompt for a specific message.
386 * Shifts down other itemized prompts as necessary.
387 * @param {number} messageId Message ID to delete itemized prompt for
388 */
389export function deleteItemizedPromptForMessage(messageId) {
390 if (!Array.isArray(itemizedPrompts)) {
391 return;
392 }
393
394 itemizedPrompts = itemizedPrompts.filter(x => x.mesId !== messageId);
395
396 for (const prompt of itemizedPrompts.filter(x => x.mesId > messageId)) {
397 prompt.mesId -= 1;
398 }
399}