Replace object stores for Chroma and token cache

This commit is contained in:
Cohee 2023-08-14 22:19:14 +03:00
parent edcce96a6e
commit 3850e6b50a
5 changed files with 65 additions and 150 deletions

View File

@ -40,6 +40,7 @@
<script src="scripts/select2.min.js"></script> <script src="scripts/select2.min.js"></script>
<script src="scripts/seedrandom.min.js"></script> <script src="scripts/seedrandom.min.js"></script>
<script src="scripts/droll.js"></script> <script src="scripts/droll.js"></script>
<script src="scripts/localforage.min.js"></script>
<script type="module" src="scripts/eventemitter.js"></script> <script type="module" src="scripts/eventemitter.js"></script>
<script type="module" src="scripts/power-user.js"></script> <script type="module" src="scripts/power-user.js"></script>
<script type="module" src="scripts/swiped-events.js"></script> <script type="module" src="scripts/swiped-events.js"></script>

View File

@ -1,11 +1,11 @@
import { saveSettingsDebounced, getCurrentChatId, system_message_types, extension_prompt_types, eventSource, event_types, getRequestHeaders, CHARACTERS_PER_TOKEN_RATIO, substituteParams, max_context, } from "../../../script.js"; import { saveSettingsDebounced, getCurrentChatId, system_message_types, extension_prompt_types, eventSource, event_types, getRequestHeaders, CHARACTERS_PER_TOKEN_RATIO, substituteParams, max_context, } from "../../../script.js";
import { humanizedDateTime } from "../../RossAscends-mods.js"; import { humanizedDateTime } from "../../RossAscends-mods.js";
import { getApiUrl, extension_settings, getContext, doExtrasFetch } from "../../extensions.js"; import { getApiUrl, extension_settings, getContext, doExtrasFetch } from "../../extensions.js";
import { getFileText, onlyUnique, splitRecursive, IndexedDBStore } from "../../utils.js"; import { getFileText, onlyUnique, splitRecursive } from "../../utils.js";
export { MODULE_NAME }; export { MODULE_NAME };
const MODULE_NAME = 'chromadb'; const MODULE_NAME = 'chromadb';
const dbStore = new IndexedDBStore('SillyTavern', MODULE_NAME); const dbStore = localforage.createInstance({ name: 'SillyTavern_ChromaDB' });
const defaultSettings = { const defaultSettings = {
strategy: 'original', strategy: 'original',
@ -59,7 +59,7 @@ async function invalidateMessageSyncState(messageId) {
console.log('CHROMADB: invalidating message sync state', messageId); console.log('CHROMADB: invalidating message sync state', messageId);
const state = await getChatSyncState(); const state = await getChatSyncState();
state[messageId] = 0; state[messageId] = 0;
await dbStore.put(getCurrentChatId(), state); await dbStore.setItem(getCurrentChatId(), state);
} }
async function getChatSyncState() { async function getChatSyncState() {
@ -69,7 +69,7 @@ async function getChatSyncState() {
} }
const context = getContext(); const context = getContext();
const chatState = (await dbStore.get(currentChatId)) || []; const chatState = (await dbStore.getItem(currentChatId)) || [];
// if the chat length has decreased, it means that some messages were deleted // if the chat length has decreased, it means that some messages were deleted
if (chatState.length > context.chat.length) { if (chatState.length > context.chat.length) {
@ -92,7 +92,7 @@ async function getChatSyncState() {
chatState[i] = 0; chatState[i] = 0;
} }
} }
await dbStore.put(currentChatId, chatState); await dbStore.setItem(currentChatId, chatState);
return chatState; return chatState;
} }
@ -304,7 +304,7 @@ async function filterSyncedMessages(splitMessages) {
} }
console.debug('CHROMADB: sync state', syncState.map((v, i) => ({ id: i, synced: v }))); console.debug('CHROMADB: sync state', syncState.map((v, i) => ({ id: i, synced: v })));
await dbStore.put(getCurrentChatId(), syncState); await dbStore.setItem(getCurrentChatId(), syncState);
// remove messages that are already synced // remove messages that are already synced
return splitMessages.filter((_, i) => !removeIndices.includes(i)); return splitMessages.filter((_, i) => !removeIndices.includes(i));
@ -325,7 +325,7 @@ async function onPurgeClick() {
}); });
if (purgeResult.ok) { if (purgeResult.ok) {
await dbStore.delete(chat_id); await dbStore.removeItem(chat_id);
toastr.success('ChromaDB context has been successfully cleared'); toastr.success('ChromaDB context has been successfully cleared');
} }
} }

7
public/scripts/localforage.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -26,7 +26,7 @@ import {
event_types, event_types,
substituteParams, substituteParams,
} from "../script.js"; } from "../script.js";
import {groups, selected_group} from "./group-chats.js"; import { groups, selected_group } from "./group-chats.js";
import { import {
promptManagerDefaultPromptOrders, promptManagerDefaultPromptOrders,
@ -45,7 +45,6 @@ import {
} from "./secrets.js"; } from "./secrets.js";
import { import {
IndexedDBStore,
delay, delay,
download, download,
getFileText, getFileText,
@ -121,35 +120,36 @@ const openrouter_website_model = 'OR_Website';
let biasCache = undefined; let biasCache = undefined;
let model_list = []; let model_list = [];
const objectStore = new IndexedDBStore('SillyTavern', 'chat_completions'); const objectStore = new localforage.createInstance({ name: "SillyTavern_ChatCompletions" });
let tokenCache = {}; let tokenCache = {};
async function loadTokenCache() { async function loadTokenCache() {
try { try {
console.debug('Chat Completions: loading token cache from IndexedDB') console.debug('Chat Completions: loading token cache')
tokenCache = await objectStore.get('tokenCache') || {}; tokenCache = await objectStore.getItem('tokenCache') || {};
} catch (e) { } catch (e) {
console.log('Chat Completions: unable to load token cache from IndexedDB, using default value', e); console.log('Chat Completions: unable to load token cache, using default value', e);
tokenCache = {}; tokenCache = {};
} }
} }
async function saveTokenCache() { async function saveTokenCache() {
try { try {
console.debug('Chat Completions: saving token cache to IndexedDB') console.debug('Chat Completions: saving token cache')
await objectStore.put('tokenCache', tokenCache); await objectStore.setItem('tokenCache', tokenCache);
} catch (e) { } catch (e) {
console.log('Chat Completions: unable to save token cache to IndexedDB', e); console.log('Chat Completions: unable to save token cache', e);
} }
} }
async function resetTokenCache() { async function resetTokenCache() {
try { try {
console.debug('Chat Completions: resetting token cache in IndexedDB'); console.debug('Chat Completions: resetting token cache');
Object.keys(tokenCache).forEach(key => delete tokenCache[key]); Object.keys(tokenCache).forEach(key => delete tokenCache[key]);
await objectStore.delete('tokenCache'); await objectStore.removeItem('tokenCache');
} catch (e) { } catch (e) {
console.log('Chat Completions: unable to reset token cache in IndexedDB', e); console.log('Chat Completions: unable to reset token cache', e);
} }
} }
@ -298,7 +298,7 @@ function setOpenAIMessages(chat) {
// Apply the "wrap in quotes" option // Apply the "wrap in quotes" option
if (role == 'user' && oai_settings.wrap_in_quotes) content = `"${content}"`; if (role == 'user' && oai_settings.wrap_in_quotes) content = `"${content}"`;
const name = chat[j]['name']; const name = chat[j]['name'];
openai_msgs[i] = { "role": role, "content": content, name: name}; openai_msgs[i] = { "role": role, "content": content, name: name };
j++; j++;
} }
@ -532,7 +532,7 @@ function populateDialogueExamples(prompts, chatCompletion) {
chatCompletion.freeBudget(newExampleChat); chatCompletion.freeBudget(newExampleChat);
const chatExamples = chatCompletion.getMessages().getItemByIdentifier('dialogueExamples').getCollection(); const chatExamples = chatCompletion.getMessages().getItemByIdentifier('dialogueExamples').getCollection();
if(chatExamples.length) chatCompletion.insertAtStart(newExampleChat,'dialogueExamples'); if (chatExamples.length) chatCompletion.insertAtStart(newExampleChat, 'dialogueExamples');
} }
} }
@ -546,7 +546,7 @@ function populateDialogueExamples(prompts, chatCompletion) {
* @param {string} options.quietPrompt - Instruction prompt for extras * @param {string} options.quietPrompt - Instruction prompt for extras
* @param {string} options.type - The type of the chat, can be 'impersonate'. * @param {string} options.type - The type of the chat, can be 'impersonate'.
*/ */
function populateChatCompletion (prompts, chatCompletion, {bias, quietPrompt, type, cyclePrompt} = {}) { function populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, type, cyclePrompt } = {}) {
// Helper function for preparing a prompt, that already exists within the prompt collection, for completion // Helper function for preparing a prompt, that already exists within the prompt collection, for completion
const addToChatCompletion = (source, target = null) => { const addToChatCompletion = (source, target = null) => {
// We need the prompts array to determine a position for the source. // We need the prompts array to determine a position for the source.
@ -616,7 +616,7 @@ function populateChatCompletion (prompts, chatCompletion, {bias, quietPrompt, ty
} }
// Persona Description // Persona Description
if(power_user.persona_description) { if (power_user.persona_description) {
const personaDescription = Message.fromPrompt(prompts.get('personaDescription')); const personaDescription = Message.fromPrompt(prompts.get('personaDescription'));
try { try {
@ -678,16 +678,16 @@ function preparePromptsForChatCompletion(Scenario, charPersonality, name2, world
// Create entries for system prompts // Create entries for system prompts
const systemPrompts = [ const systemPrompts = [
// Ordered prompts for which a marker should exist // Ordered prompts for which a marker should exist
{role: 'system', content: formatWorldInfo(worldInfoBefore), identifier: 'worldInfoBefore'}, { role: 'system', content: formatWorldInfo(worldInfoBefore), identifier: 'worldInfoBefore' },
{role: 'system', content: formatWorldInfo(worldInfoAfter), identifier: 'worldInfoAfter'}, { role: 'system', content: formatWorldInfo(worldInfoAfter), identifier: 'worldInfoAfter' },
{role: 'system', content: charDescription, identifier: 'charDescription'}, { role: 'system', content: charDescription, identifier: 'charDescription' },
{role: 'system', content: charPersonalityText, identifier: 'charPersonality'}, { role: 'system', content: charPersonalityText, identifier: 'charPersonality' },
{role: 'system', content: scenarioText, identifier: 'scenario'}, { role: 'system', content: scenarioText, identifier: 'scenario' },
// Unordered prompts without marker // Unordered prompts without marker
{role: 'system', content: oai_settings.nsfw_avoidance_prompt, identifier: 'nsfwAvoidance'}, { role: 'system', content: oai_settings.nsfw_avoidance_prompt, identifier: 'nsfwAvoidance' },
{role: 'system', content: oai_settings.impersonation_prompt, identifier: 'impersonate'}, { role: 'system', content: oai_settings.impersonation_prompt, identifier: 'impersonate' },
{role: 'system', content: quietPrompt, identifier: 'quietPrompt'}, { role: 'system', content: quietPrompt, identifier: 'quietPrompt' },
{role: 'system', content: bias, identifier: 'bias'} { role: 'system', content: bias, identifier: 'bias' }
]; ];
// Tavern Extras - Summary // Tavern Extras - Summary
@ -708,7 +708,7 @@ function preparePromptsForChatCompletion(Scenario, charPersonality, name2, world
// Persona Description // Persona Description
if (power_user.persona_description) { if (power_user.persona_description) {
systemPrompts.push({role: 'system', content: power_user.persona_description, identifier: 'personaDescription'}); systemPrompts.push({ role: 'system', content: power_user.persona_description, identifier: 'personaDescription' });
} }
// This is the prompt order defined by the user // This is the prompt order defined by the user
@ -778,18 +778,18 @@ function preparePromptsForChatCompletion(Scenario, charPersonality, name2, world
* @returns {(*[]|boolean)[]} An array where the first element is the prepared chat and the second element is a boolean flag. * @returns {(*[]|boolean)[]} An array where the first element is the prepared chat and the second element is a boolean flag.
*/ */
function prepareOpenAIMessages({ function prepareOpenAIMessages({
name2, name2,
charDescription, charDescription,
charPersonality, charPersonality,
Scenario, Scenario,
worldInfoBefore, worldInfoBefore,
worldInfoAfter, worldInfoAfter,
bias, bias,
type, type,
quietPrompt, quietPrompt,
extensionPrompts, extensionPrompts,
cyclePrompt cyclePrompt
} = {}, dryRun) { } = {}, dryRun) {
// Without a character selected, there is no way to accurately calculate tokens // Without a character selected, there is no way to accurately calculate tokens
if (!promptManager.activeCharacter && dryRun) return [null, false]; if (!promptManager.activeCharacter && dryRun) return [null, false];
@ -804,13 +804,13 @@ function prepareOpenAIMessages({
const prompts = preparePromptsForChatCompletion(Scenario, charPersonality, name2, worldInfoBefore, worldInfoAfter, charDescription, quietPrompt, bias, extensionPrompts); const prompts = preparePromptsForChatCompletion(Scenario, charPersonality, name2, worldInfoBefore, worldInfoAfter, charDescription, quietPrompt, bias, extensionPrompts);
// Fill the chat completion with as much context as the budget allows // Fill the chat completion with as much context as the budget allows
populateChatCompletion(prompts, chatCompletion, {bias, quietPrompt, type, cyclePrompt}); populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, type, cyclePrompt });
} catch (error) { } catch (error) {
if (error instanceof TokenBudgetExceededError) { if (error instanceof TokenBudgetExceededError) {
toastr.error('An error occurred while counting tokens: Token budget exceeded.') toastr.error('An error occurred while counting tokens: Token budget exceeded.')
chatCompletion.log('Token budget exceeded.'); chatCompletion.log('Token budget exceeded.');
promptManager.error = 'Not enough free tokens for mandatory prompts. Raise your token Limit or disable custom prompts.'; promptManager.error = 'Not enough free tokens for mandatory prompts. Raise your token Limit or disable custom prompts.';
} else if (error instanceof InvalidCharacterNameError) { } else if (error instanceof InvalidCharacterNameError) {
toastr.warning('An error occurred while counting tokens: Invalid character name') toastr.warning('An error occurred while counting tokens: Invalid character name')
chatCompletion.log('Invalid character name'); chatCompletion.log('Invalid character name');
promptManager.error = 'The name of at least one character contained whitespaces or special characters. Please check your user and character name.'; promptManager.error = 'The name of at least one character contained whitespaces or special characters. Please check your user and character name.';
@ -1267,7 +1267,7 @@ class TokenHandler {
} }
resetCounts() { resetCounts() {
Object.keys(this.counts).forEach((key) => this.counts[key] = 0 ); Object.keys(this.counts).forEach((key) => this.counts[key] = 0);
} }
setCounts(counts) { setCounts(counts) {
@ -1397,7 +1397,7 @@ class Message {
this.content = content; this.content = content;
if (this.content) { if (this.content) {
this.tokens = tokenHandler.count({role: this.role, content: this.content}) this.tokens = tokenHandler.count({ role: this.role, content: this.content })
} else { } else {
this.tokens = 0; this.tokens = 0;
} }
@ -1421,7 +1421,7 @@ class Message {
* Returns the number of tokens in the message. * Returns the number of tokens in the message.
* @returns {number} Number of tokens in the message. * @returns {number} Number of tokens in the message.
*/ */
getTokens() {return this.tokens}; getTokens() { return this.tokens };
} }
/** /**
@ -1429,7 +1429,7 @@ class Message {
* *
* @class MessageCollection * @class MessageCollection
*/ */
class MessageCollection { class MessageCollection {
collection = []; collection = [];
identifier; identifier;
@ -1439,8 +1439,8 @@ class MessageCollection {
* @param {...Object} items - An array of Message or MessageCollection instances to be added to the collection. * @param {...Object} items - An array of Message or MessageCollection instances to be added to the collection.
*/ */
constructor(identifier, ...items) { constructor(identifier, ...items) {
for(let item of items) { for (let item of items) {
if(!(item instanceof Message || item instanceof MessageCollection)) { if (!(item instanceof Message || item instanceof MessageCollection)) {
throw new Error('Only Message and MessageCollection instances can be added to MessageCollection'); throw new Error('Only Message and MessageCollection instances can be added to MessageCollection');
} }
} }
@ -1456,7 +1456,7 @@ class MessageCollection {
getChat() { getChat() {
return this.collection.reduce((acc, message) => { return this.collection.reduce((acc, message) => {
const name = message.name; const name = message.name;
if (message.content) acc.push({role: message.role, ...(name && { name }), content: message.content}); if (message.content) acc.push({ role: message.role, ...(name && { name }), content: message.content });
return acc; return acc;
}, []); }, []);
} }
@ -2450,7 +2450,7 @@ function onSettingsPresetChange() {
settingsToUpdate: settingsToUpdate, settingsToUpdate: settingsToUpdate,
settings: oai_settings, settings: oai_settings,
savePreset: saveOpenAIPreset savePreset: saveOpenAIPreset
}).finally(r =>{ }).finally(r => {
for (const [key, [selector, setting, isCheckbox]] of Object.entries(settingsToUpdate)) { for (const [key, [selector, setting, isCheckbox]] of Object.entries(settingsToUpdate)) {
if (preset[key] !== undefined) { if (preset[key] !== undefined) {
if (isCheckbox) { if (isCheckbox) {
@ -2799,8 +2799,8 @@ function onProxyPasswordShowClick() {
$(this).toggleClass('fa-eye-slash fa-eye'); $(this).toggleClass('fa-eye-slash fa-eye');
} }
$(document).ready(function () { $(document).ready(async function () {
loadTokenCache(); await loadTokenCache();
$('#test_api_button').on('click', testApiConnection); $('#test_api_button').on('click', testApiConnection);

View File

@ -370,99 +370,6 @@ export function splitRecursive(input, length, delimitiers = ['\n\n', '\n', ' ',
return result; return result;
} }
export class IndexedDBStore {
constructor(dbName, storeName) {
this.dbName = dbName;
this.storeName = storeName;
this.db = null;
this.version = Date.now();
}
async open() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName, { keyPath: null, autoIncrement: false });
}
};
request.onsuccess = (event) => {
console.debug(`IndexedDBStore.open(${this.dbName})`);
this.db = event.target.result;
resolve(this.db);
};
request.onerror = (event) => {
console.error(`IndexedDBStore.open(${this.dbName})`);
reject(event.target.error);
};
});
}
async get(key) {
if (!this.db) await this.open();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, "readonly");
const objectStore = transaction.objectStore(this.storeName);
const request = objectStore.get(key);
request.onsuccess = (event) => {
console.debug(`IndexedDBStore.get(${key})`);
resolve(event.target.result);
};
request.onerror = (event) => {
console.error(`IndexedDBStore.get(${key})`);
reject(event.target.error);
};
});
}
async put(key, object) {
if (!this.db) await this.open();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, "readwrite");
const objectStore = transaction.objectStore(this.storeName);
const request = objectStore.put(object, key);
request.onsuccess = (event) => {
console.debug(`IndexedDBStore.put(${key})`);
resolve(event.target.result);
};
request.onerror = (event) => {
console.error(`IndexedDBStore.put(${key})`);
reject(event.target.error);
};
});
}
async delete(key) {
if (!this.db) await this.open();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, "readwrite");
const objectStore = transaction.objectStore(this.storeName);
const request = objectStore.delete(key);
request.onsuccess = (event) => {
console.debug(`IndexedDBStore.delete(${key})`);
resolve(event.target.result);
};
request.onerror = (event) => {
console.error(`IndexedDBStore.delete(${key})`);
reject(event.target.error);
};
});
}
}
export function isDataURL(str) { export function isDataURL(str) {
const regex = /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)*;?)?(base64)?,([a-z0-9!$&',()*+;=\-_%.~:@\/?#]+)?$/i; const regex = /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)*;?)?(base64)?,([a-z0-9!$&',()*+;=\-_%.~:@\/?#]+)?$/i;
return regex.test(str); return regex.test(str);