import { saveSettingsDebounced, getCurrentChatId, system_message_types, eventSource, event_types, getRequestHeaders, CHARACTERS_PER_TOKEN_RATIO, substituteParams, } from "../../../script.js"; import { humanizedDateTime } from "../../RossAscends-mods.js"; import { getApiUrl, extension_settings, getContext, doExtrasFetch } from "../../extensions.js"; import { getFileText, onlyUnique, splitRecursive, IndexedDBStore } from "../../utils.js"; export { MODULE_NAME }; const MODULE_NAME = 'chromadb'; const dbStore = new IndexedDBStore('SillyTavern', MODULE_NAME); const defaultSettings = { strategy: 'original', sort_strategy: 'date', keep_context: 10, keep_context_min: 1, keep_context_max: 500, keep_context_step: 1, n_results: 20, n_results_min: 0, n_results_max: 500, n_results_step: 1, chroma_depth: 20, chroma_depth_min: -1, chroma_depth_max: 500, chroma_depth_step: 1, chroma_default_msg: "In a past conversation: [{{memories}}]", split_length: 384, split_length_min: 64, split_length_max: 4096, split_length_step: 64, file_split_length: 1024, file_split_length_min: 512, file_split_length_max: 4096, file_split_length_step: 128, keep_context_proportion: 0.5, keep_context_proportion_min: 0.0, keep_context_proportion_max: 1.0, keep_context_proportion_step: 0.05, auto_adjust: true, freeze: false, }; const postHeaders = { 'Content-Type': 'application/json', 'Bypass-Tunnel-Reminder': 'bypass', }; async function invalidateMessageSyncState(messageId) { console.log('CHROMADB: invalidating message sync state', messageId); const state = await getChatSyncState(); state[messageId] = 0; await dbStore.put(getCurrentChatId(), state); } async function getChatSyncState() { const currentChatId = getCurrentChatId(); if (!checkChatId(currentChatId)) { return; } const context = getContext(); const chatState = (await dbStore.get(currentChatId)) || []; // if the chat length has decreased, it means that some messages were deleted if (chatState.length > context.chat.length) { for (let i = context.chat.length; i < chatState.length; i++) { // if the synced message was deleted, notify the user if (chatState[i]) { toastr.warning( 'Purge your ChromaDB to remove it from there too. See the "Smart Context" tab in the Extensions menu for more information.', 'Message deleted from chat, but it still exists inside the ChromaDB database.', { timeOut: 0, extendedTimeOut: 0, preventDuplicates: true }, ); break; } } } chatState.length = context.chat.length; for (let i = 0; i < chatState.length; i++) { if (chatState[i] === undefined) { chatState[i] = 0; } } await dbStore.put(currentChatId, chatState); return chatState; } async function loadSettings() { if (Object.keys(extension_settings.chromadb).length === 0) { Object.assign(extension_settings.chromadb, defaultSettings); } console.debug(`loading chromadb strat:${extension_settings.chromadb.strategy}`); $("#chromadb_strategy option[value=" + extension_settings.chromadb.strategy + "]").attr( "selected", "true" ); $("#chromadb_sort_strategy option[value=" + extension_settings.chromadb.sort_strategy + "]").attr( "selected", "true" ); $('#chromadb_keep_context').val(extension_settings.chromadb.keep_context).trigger('input'); $('#chromadb_n_results').val(extension_settings.chromadb.n_results).trigger('input'); $('#chromadb_split_length').val(extension_settings.chromadb.split_length).trigger('input'); $('#chromadb_file_split_length').val(extension_settings.chromadb.file_split_length).trigger('input'); $('#chromadb_keep_context_proportion').val(extension_settings.chromadb.keep_context_proportion).trigger('input'); $('#chromadb_custom_depth').val(extension_settings.chromadb.chroma_depth).trigger('input'); $('#chromadb_custom_msg').val(extension_settings.chromadb.recall_msg).trigger('input'); $('#chromadb_auto_adjust').prop('checked', extension_settings.chromadb.auto_adjust); $('#chromadb_freeze').prop('checked', extension_settings.chromadb.freeze); enableDisableSliders(); onStrategyChange(); } function onStrategyChange() { console.debug('changing chromadb strat'); extension_settings.chromadb.strategy = $('#chromadb_strategy').val(); if(extension_settings.chromadb.strategy === "custom"){ $('#chromadb_custom_depth').show(); $('label[for="chromadb_custom_depth"]').show(); $('#chromadb_custom_msg').show(); $('label[for="chromadb_custom_msg"]').show(); } else { $('#chromadb_custom_depth').hide(); $('label[for="chromadb_custom_depth"]').hide(); $('#chromadb_custom_msg').hide(); $('label[for="chromadb_custom_msg"]').hide(); } saveSettingsDebounced(); } function onRecallStrategyChange() { console.log('changing chromadb recall strat'); extension_settings.chromadb.recall_strategy = $('#chromadb_recall_strategy').val(); saveSettingsDebounced(); } function onSortStrategyChange() { console.log('changing chromadb sort strat'); extension_settings.chromadb.sort_strategy = $('#chromadb_sort_strategy').val(); saveSettingsDebounced(); } function onKeepContextInput() { extension_settings.chromadb.keep_context = Number($('#chromadb_keep_context').val()); $('#chromadb_keep_context_value').text(extension_settings.chromadb.keep_context); saveSettingsDebounced(); } function onNResultsInput() { extension_settings.chromadb.n_results = Number($('#chromadb_n_results').val()); $('#chromadb_n_results_value').text(extension_settings.chromadb.n_results); saveSettingsDebounced(); } function onChromaDepthInput() { extension_settings.chromadb.chroma_depth = Number($('#chromadb_custom_depth').val()); $('#chromadb_custom_depth_value').text(extension_settings.chromadb.chroma_depth); saveSettingsDebounced(); } function onChromaMsgInput() { extension_settings.chromadb.recall_msg = $('#chromadb_custom_msg').val(); saveSettingsDebounced(); } function onSplitLengthInput() { extension_settings.chromadb.split_length = Number($('#chromadb_split_length').val()); $('#chromadb_split_length_value').text(extension_settings.chromadb.split_length); saveSettingsDebounced(); } function onFileSplitLengthInput() { extension_settings.chromadb.file_split_length = Number($('#chromadb_file_split_length').val()); $('#chromadb_file_split_length_value').text(extension_settings.chromadb.file_split_length); saveSettingsDebounced(); } function checkChatId(chat_id) { if (!chat_id || chat_id.trim() === '') { toastr.error('Please select a character and try again.'); return false; } return true; } async function addMessages(chat_id, messages) { if (extension_settings.chromadb.freeze) { return { count: 0 }; } const url = new URL(getApiUrl()); url.pathname = '/api/chromadb'; const messagesDeepCopy = JSON.parse(JSON.stringify(messages)); let splitMessages = []; let id = 0; messagesDeepCopy.forEach((m, index) => { const split = splitRecursive(m.mes, extension_settings.chromadb.split_length); splitMessages.push(...split.map(text => ({ ...m, mes: text, send_date: id, id: `msg-${id++}`, index: index, extra: undefined, }))); }); splitMessages = await filterSyncedMessages(splitMessages); // no messages to add if (splitMessages.length === 0) { return { count: 0 }; } const transformedMessages = splitMessages.map((m) => ({ id: m.id, role: m.is_user ? 'user' : 'assistant', content: m.mes, date: m.send_date, meta: JSON.stringify(m), })); const addMessagesResult = await doExtrasFetch(url, { method: 'POST', headers: postHeaders, body: JSON.stringify({ chat_id, messages: transformedMessages }), }); if (addMessagesResult.ok) { const addMessagesData = await addMessagesResult.json(); return addMessagesData; // { count: 1 } } return { count: 0 }; } async function filterSyncedMessages(splitMessages) { const syncState = await getChatSyncState(); const removeIndices = []; const syncedIndices = []; for (let i = 0; i < splitMessages.length; i++) { const index = splitMessages[i].index; if (syncState[index]) { removeIndices.push(i); continue; } syncedIndices.push(index); } for (const index of syncedIndices) { syncState[index] = 1; } console.debug('CHROMADB: sync state', syncState.map((v, i) => ({ id: i, synced: v }))); await dbStore.put(getCurrentChatId(), syncState); // remove messages that are already synced return splitMessages.filter((_, i) => !removeIndices.includes(i)); } async function onPurgeClick() { const chat_id = getCurrentChatId(); if (!checkChatId(chat_id)) { return; } const url = new URL(getApiUrl()); url.pathname = '/api/chromadb/purge'; const purgeResult = await doExtrasFetch(url, { method: 'POST', headers: postHeaders, body: JSON.stringify({ chat_id }), }); if (purgeResult.ok) { await dbStore.delete(chat_id); toastr.success('ChromaDB context has been successfully cleared'); } } async function onExportClick() { const currentChatId = getCurrentChatId(); if (!checkChatId(currentChatId)) { return; } const url = new URL(getApiUrl()); url.pathname = '/api/chromadb/export'; const exportResult = await doExtrasFetch(url, { method: 'POST', headers: postHeaders, body: JSON.stringify({ chat_id: currentChatId }), }); if (exportResult.ok) { const data = await exportResult.json(); const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const href = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = href; link.download = currentChatId + '.json'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } else { toastr.error('An error occurred while attempting to download the data'); } } async function onSelectImportFile(e) { const file = e.target.files[0]; const currentChatId = getCurrentChatId(); if (!checkChatId(currentChatId)) { return; } if (!file) { return; } try { toastr.info('This may take some time, depending on the file size', 'Processing...'); const text = await getFileText(file); const imported = JSON.parse(text); imported.chat_id = currentChatId; const url = new URL(getApiUrl()); url.pathname = '/api/chromadb/import'; const importResult = await doExtrasFetch(url, { method: 'POST', headers: postHeaders, body: JSON.stringify(imported), }); if (importResult.ok) { const importResultData = await importResult.json(); toastr.success(`Number of chunks: ${importResultData.count}`, 'Injected successfully!'); return importResultData; } else { throw new Error(); } } catch (error) { console.log(error); toastr.error('Something went wrong while importing the data'); } finally { e.target.form.reset(); } } async function queryMessages(chat_id, query) { const url = new URL(getApiUrl()); url.pathname = '/api/chromadb/query'; const queryMessagesResult = await doExtrasFetch(url, { method: 'POST', headers: postHeaders, body: JSON.stringify({ chat_id, query, n_results: extension_settings.chromadb.n_results }), }); if (queryMessagesResult.ok) { const queryMessagesData = await queryMessagesResult.json(); return queryMessagesData; } return []; } async function queryMultiMessages(chat_id, query) { const context = getContext(); const response = await fetch("/getallchatsofcharacter", { method: 'POST', body: JSON.stringify({ avatar_url: context.characters[context.characterId].avatar}), headers: getRequestHeaders(), }); if (!response.ok) { return; } let data = await response.json(); data = Object.values(data); let chat_list = data.sort((a, b) => a["file_name"].localeCompare(b["file_name"])).reverse(); // Extracting chat_ids from the chat_list chat_list = chat_list.map(chat => chat.file_name.replace(/\.[^/.]+$/, "")); const url = new URL(getApiUrl()); url.pathname = '/api/chromadb/multiquery'; const queryMessagesResult = await fetch(url, { method: 'POST', body: JSON.stringify({ chat_list, query, n_results: extension_settings.chromadb.n_results }), headers: postHeaders, }); if (queryMessagesResult.ok) { const queryMessagesData = await queryMessagesResult.json(); return queryMessagesData; } return []; } async function onSelectInjectFile(e) { const file = e.target.files[0]; const currentChatId = getCurrentChatId(); if (!checkChatId(currentChatId)) { return; } if (!file) { return; } try { toastr.info('This may take some time, depending on the file size', 'Processing...'); const text = await getFileText(file); const split = splitRecursive(text, extension_settings.chromadb.file_split_length).filter(onlyUnique); const baseDate = Date.now(); const messages = split.map((m, i) => ({ id: `${file.name}-${split.indexOf(m)}`, role: 'system', content: m, date: baseDate + i, meta: JSON.stringify({ name: file.name, is_user: false, is_name: false, is_system: false, send_date: humanizedDateTime(), mes: m, extra: { type: system_message_types.NARRATOR, } }), })); const url = new URL(getApiUrl()); url.pathname = '/api/chromadb'; const addMessagesResult = await doExtrasFetch(url, { method: 'POST', headers: postHeaders, body: JSON.stringify({ chat_id: currentChatId, messages: messages }), }); if (addMessagesResult.ok) { const addMessagesData = await addMessagesResult.json(); toastr.success(`Number of chunks: ${addMessagesData.count}`, 'Injected successfully!'); return addMessagesData; } else { throw new Error(); } } catch (error) { console.log(error); toastr.error('Something went wrong while injecting the data'); } finally { e.target.form.reset(); } } /* * Automatically adjusts the extension settings for the optimal number of messages to keep and query based * on the chat history and a specified maximum context length. */ function doAutoAdjust(chat, maxContext) { console.debug('CHROMADB: Auto-adjusting sliders (messages: %o, maxContext: %o)', chat.length, maxContext); // Get mean message length const meanMessageLength = chat.reduce((acc, cur) => acc + cur.mes.length, 0) / chat.length; if (Number.isNaN(meanMessageLength)) { console.debug('CHROMADB: Mean message length is NaN, aborting auto-adjust'); return; } console.debug('CHROMADB: Mean message length (characters): %o', meanMessageLength); // Convert to number of "tokens" const meanMessageLengthTokens = Math.ceil(meanMessageLength / CHARACTERS_PER_TOKEN_RATIO); console.debug('CHROMADB: Mean message length (tokens): %o', meanMessageLengthTokens); // Get number of messages in context const contextMessages = Math.max(1, Math.ceil(maxContext / meanMessageLengthTokens)); // Round up to nearest 10 const contextMessagesRounded = Math.ceil(contextMessages / 10) * 10; console.debug('CHROMADB: Estimated context messages (rounded): %o', contextMessagesRounded); // Messages to keep (proportional, rounded to nearest 5, minimum 10, maximum 500) const messagesToKeep = Math.min(defaultSettings.keep_context_max, Math.max(10, Math.ceil(contextMessagesRounded * extension_settings.chromadb.keep_context_proportion / 5) * 5)); console.debug('CHROMADB: Estimated messages to keep: %o', messagesToKeep); // Messages to query (rounded, maximum 500) const messagesToQuery = Math.min(defaultSettings.n_results_max, contextMessagesRounded - messagesToKeep); console.debug('CHROMADB: Estimated messages to query: %o', messagesToQuery); // Set extension settings extension_settings.chromadb.keep_context = messagesToKeep; extension_settings.chromadb.n_results = messagesToQuery; // Update sliders $('#chromadb_keep_context').val(messagesToKeep); $('#chromadb_n_results').val(messagesToQuery); // Update labels $('#chromadb_keep_context_value').text(extension_settings.chromadb.keep_context); $('#chromadb_n_results_value').text(extension_settings.chromadb.n_results); } window.chromadb_interceptGeneration = async (chat, maxContext) => { if (extension_settings.chromadb.auto_adjust) { doAutoAdjust(chat, maxContext); } const currentChatId = getCurrentChatId(); const selectedStrategy = extension_settings.chromadb.strategy; const recallStrategy = extension_settings.chromadb.recall_strategy; let recallMsg = extension_settings.chromadb.recall_msg || defaultSettings.chroma_default_msg; const chromaDepth = extension_settings.chromadb.chroma_depth; const chromaSortStrategy = extension_settings.chromadb.sort_strategy; if (currentChatId) { const messagesToStore = chat.slice(0, -extension_settings.chromadb.keep_context); if (messagesToStore.length > 0 || extension_settings.chromadb.freeze) { await addMessages(currentChatId, messagesToStore); const lastMessage = chat[chat.length - 1]; let queriedMessages; console.debug(recallStrategy) if (lastMessage) { if (recallStrategy === 'multichat'){ console.log("Utilizing multichat") queriedMessages = await queryMultiMessages(currentChatId, lastMessage.mes); } else{ queriedMessages = await queryMessages(currentChatId, lastMessage.mes); } if(chromaSortStrategy === "date"){ queriedMessages.sort((a, b) => a.date - b.date); } else{ queriedMessages.sort((a, b) => b.distance - a.distance); } let newChat = []; if (selectedStrategy === 'ross') { //adds chroma to the end of chat and allows Generate() to cull old messages naturally. const context = getContext(); const charname = context.name2; newChat.push( { is_name: false, is_user: false, mes: `[Use these past chat exchanges to inform ${charname}'s next response:`, name: "system", send_date: 0, } ); newChat.push(...queriedMessages.map(m => m.meta).filter(onlyUnique).map(JSON.parse)); newChat.push( { is_name: false, is_user: false, mes: `]\n`, name: "system", send_date: 0, } ); chat.splice(chat.length, 0, ...newChat); } if (selectedStrategy === 'custom') { const context = getContext(); recallMsg = substituteParams(recallMsg, context.name1, context.name2); if (!text.includes("{{memories}}")) { text += " {{memories}}"; } let recallStart = recallMsg.split('{{memories}}')[0] let recallEnd = recallMsg.split('{{memories}}')[1] newChat.push( { is_name: false, is_user: false, mes: recallStart, name: "system", send_date: 0, } ); newChat.push(...queriedMessages.map(m => m.meta).filter(onlyUnique).map(JSON.parse)); newChat.push( { is_name: false, is_user: false, mes: recallEnd + `\n`, name: "system", send_date: 0, } ); //prototype chroma duplicate removal let chatset = new Set(chat.map(obj => obj.mes)); newChat = newChat.filter(obj => !chatset.has(obj.mes)); if(chromaDepth === -1){ chat.splice(chat.length, 0, ...newChat); } else{ chat.splice(chromaDepth, 0, ...newChat); } } if (selectedStrategy === 'original') { //removes .length # messages from the start of 'kept messages' //replaces them with chromaDB results (with no separator) newChat.push(...queriedMessages.map(m => m.meta).filter(onlyUnique).map(JSON.parse)); chat.splice(0, messagesToStore.length, ...newChat); } console.log('ChromaDB chat after injection', chat); } } } } function onFreezeInput() { extension_settings.chromadb.freeze = $('#chromadb_freeze').is(':checked'); saveSettingsDebounced(); } function onAutoAdjustInput() { extension_settings.chromadb.auto_adjust = $('#chromadb_auto_adjust').is(':checked'); enableDisableSliders(); saveSettingsDebounced(); } function enableDisableSliders() { if (extension_settings.chromadb.auto_adjust) { $('#chromadb_keep_context').prop('disabled', true).css('opacity', 0.5); $('#chromadb_n_results').prop('disabled', true).css('opacity', 0.5); $('#chromadb_keep_context_proportion').prop('disabled', false).css('opacity', 1); } else { $('#chromadb_keep_context').prop('disabled', false).css('opacity', 1); $('#chromadb_n_results').prop('disabled', false).css('opacity', 1); $('#chromadb_keep_context_proportion').prop('disabled', true).css('opacity', 0.5); } } function onKeepContextProportionInput() { extension_settings.chromadb.keep_context_proportion = $('#chromadb_keep_context_proportion').val(); $('#chromadb_keep_context_proportion_value').text(Math.round(extension_settings.chromadb.keep_context_proportion * 100)); saveSettingsDebounced(); } jQuery(async () => { const settingsHtml = `