| 1 | import { |
| 2 | eventSource, |
| 3 | event_types, |
| 4 | extension_prompt_types, |
| 5 | extension_prompt_roles, |
| 6 | getCurrentChatId, |
| 7 | getRequestHeaders, |
| 8 | is_send_press, |
| 9 | saveSettingsDebounced, |
| 10 | setExtensionPrompt, |
| 11 | substituteParams, |
| 12 | generateRaw, |
| 13 | substituteParamsExtended, |
| 14 | } from '../../../script.js'; |
| 15 | import { |
| 16 | ModuleWorkerWrapper, |
| 17 | extension_settings, |
| 18 | getContext, |
| 19 | modules, |
| 20 | renderExtensionTemplateAsync, |
| 21 | doExtrasFetch, getApiUrl, |
| 22 | openThirdPartyExtensionMenu, |
| 23 | } from '../../extensions.js'; |
| 24 | import { collapseNewlines, registerDebugFunction } from '../../power-user.js'; |
| 25 | import { SECRET_KEYS, secret_state } from '../../secrets.js'; |
| 26 | import { getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment } from '../../chats.js'; |
| 27 | import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive, trimToStartSentence, trimToEndSentence, escapeHtml, isTrueBoolean } from '../../utils.js'; |
| 28 | import { debounce_timeout } from '../../constants.js'; |
| 29 | import { getSortedEntries } from '../../world-info.js'; |
| 30 | import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js'; |
| 31 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; |
| 32 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
| 33 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; |
| 34 | import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; |
| 35 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 36 | import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js'; |
| 37 | import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js'; |
| 38 | import { WebLlmVectorProvider } from './webllm.js'; |
| 39 | import { removeReasoningFromString } from '../../reasoning.js'; |
| 40 | import { oai_settings } from '../../openai.js'; |
| 41 | |
| 42 | /** |
| 43 | * @typedef {object} HashedMessage |
| 44 | * @property {string} text - The hashed message text |
| 45 | * @property {number} hash - The hash used as the vector key |
| 46 | * @property {number} index - The index of the message in the chat |
| 47 | * @property {boolean} [summaryFailed] - Whether summarization failed for this message (used internally to skip messages that fail summarization) |
| 48 | */ |
| 49 | |
| 50 | const MODULE_NAME = 'vectors'; |
| 51 | |
| 52 | export const EXTENSION_PROMPT_TAG = '3_vectors'; |
| 53 | export const EXTENSION_PROMPT_TAG_DB = '4_vectors_data_bank'; |
| 54 | |
| 55 | // Force solo chunks for sources that don't support batching. |
| 56 | const getBatchSize = () => ['transformers', 'ollama'].includes(settings.source) ? 1 : 5; |
| 57 | |
| 58 | const settings = { |
| 59 | // For both |
| 60 | source: 'transformers', |
| 61 | alt_endpoint_url: '', |
| 62 | use_alt_endpoint: false, |
| 63 | include_wi: false, |
| 64 | togetherai_model: 'togethercomputer/m2-bert-80M-32k-retrieval', |
| 65 | openai_model: 'text-embedding-ada-002', |
| 66 | electronhub_model: 'text-embedding-3-small', |
| 67 | openrouter_model: 'openai/text-embedding-3-large', |
| 68 | cohere_model: 'embed-english-v3.0', |
| 69 | ollama_model: 'mxbai-embed-large', |
| 70 | ollama_keep: false, |
| 71 | vllm_model: '', |
| 72 | webllm_model: '', |
| 73 | google_model: 'text-embedding-005', |
| 74 | chutes_model: 'chutes-qwen-qwen3-embedding-8b', |
| 75 | nanogpt_model: 'text-embedding-3-small', |
| 76 | siliconflow_model: 'Qwen/Qwen3-Embedding-0.6B', |
| 77 | summarize: false, |
| 78 | summarize_sent: false, |
| 79 | summary_source: 'main', |
| 80 | summary_prompt: 'Ignore previous instructions. Summarize the most important parts of the message. Limit yourself to 250 words or less. Your response should include nothing but the summary.', |
| 81 | summary_retries: 2, |
| 82 | summary_threshold: 200, |
| 83 | force_chunk_delimiter: '', |
| 84 | |
| 85 | // For chats |
| 86 | enabled_chats: false, |
| 87 | keep_hidden: false, |
| 88 | template: 'Past events:\n{{text}}', |
| 89 | depth: 2, |
| 90 | position: extension_prompt_types.IN_PROMPT, |
| 91 | protect: 5, |
| 92 | insert: 3, |
| 93 | query: 2, |
| 94 | message_chunk_size: 400, |
| 95 | score_threshold: 0.25, |
| 96 | |
| 97 | // For files |
| 98 | enabled_files: false, |
| 99 | translate_files: false, |
| 100 | size_threshold: 10, |
| 101 | chunk_size: 5000, |
| 102 | chunk_count: 2, |
| 103 | overlap_percent: 0, |
| 104 | only_custom_boundary: false, |
| 105 | |
| 106 | // For Data Bank |
| 107 | size_threshold_db: 5, |
| 108 | chunk_size_db: 2500, |
| 109 | chunk_count_db: 5, |
| 110 | overlap_percent_db: 0, |
| 111 | file_template_db: 'Related information:\n{{text}}', |
| 112 | file_position_db: extension_prompt_types.IN_PROMPT, |
| 113 | file_depth_db: 4, |
| 114 | file_depth_role_db: extension_prompt_roles.SYSTEM, |
| 115 | |
| 116 | // For World Info |
| 117 | enabled_world_info: false, |
| 118 | enabled_for_all: false, |
| 119 | max_entries: 5, |
| 120 | }; |
| 121 | |
| 122 | const moduleWorker = new ModuleWorkerWrapper(synchronizeChat); |
| 123 | const webllmProvider = new WebLlmVectorProvider(); |
| 124 | /** |
| 125 | * Cache for storing summaries of messages by their hash. |
| 126 | * @type {Map<number, string>} |
| 127 | */ |
| 128 | const cachedSummaries = new Map(); |
| 129 | /** |
| 130 | * Hashes skipped this Vectorize All session (summary or embed failure). Cleared on next Vectorize All click. |
| 131 | * @type {Set<number>} |
| 132 | */ |
| 133 | const skippedHashes = new Set(); |
| 134 | /** |
| 135 | * Error causes treated as fatal — abort Vectorize All rather than skip. |
| 136 | * @type {Set<string>} |
| 137 | */ |
| 138 | const FATAL_CAUSES = new Set(['account_id_missing', 'api_key_missing', 'api_url_missing', 'api_model_missing', 'extras_module_missing', 'webllm_not_supported', 'summary_endpoint_invalid']); |
| 139 | const vectorApiRequiresUrl = ['llamacpp', 'vllm', 'ollama', 'koboldcpp']; |
| 140 | |
| 141 | /** |
| 142 | * @typedef {object} RemoteEmbeddingEndpointConfig |
| 143 | * @property {string} url - The API endpoint URL |
| 144 | * @property {string} settingsKey - The key in settings for the selected model |
| 145 | * @property {string} selectId - The ID of the select element (without #) |
| 146 | * @property {string} [valueProperty='id'] - Property name for the option value |
| 147 | * @property {string} [textProperty] - Property name for the option text. Falls back to valueProperty |
| 148 | * @property {() => object} [getBody] - Function returning the request body |
| 149 | * @property {(models: any[]) => any[]} [filter] - Optional post-fetch filter for models |
| 150 | */ |
| 151 | |
| 152 | /** @type {Record<string, RemoteEmbeddingEndpointConfig>} */ |
| 153 | const remoteEmbeddingEndpoints = { |
| 154 | chutes: { |
| 155 | url: '/api/openai/chutes/models/embedding', |
| 156 | settingsKey: 'chutes_model', |
| 157 | selectId: 'vectors_chutes_model', |
| 158 | valueProperty: 'slug', |
| 159 | textProperty: 'name', |
| 160 | }, |
| 161 | nanogpt: { |
| 162 | url: '/api/openai/nanogpt/models/embedding', |
| 163 | settingsKey: 'nanogpt_model', |
| 164 | selectId: 'vectors_nanogpt_model', |
| 165 | textProperty: 'name', |
| 166 | }, |
| 167 | electronhub: { |
| 168 | url: '/api/openai/electronhub/models', |
| 169 | settingsKey: 'electronhub_model', |
| 170 | selectId: 'vectors_electronhub_model', |
| 171 | textProperty: 'name', |
| 172 | filter: models => models.filter(m => Array.isArray(m?.endpoints) && m.endpoints.includes('/v1/embeddings')), |
| 173 | }, |
| 174 | openrouter: { |
| 175 | url: '/api/openrouter/models/embedding', |
| 176 | settingsKey: 'openrouter_model', |
| 177 | selectId: 'vectors_openrouter_model', |
| 178 | textProperty: 'name', |
| 179 | }, |
| 180 | siliconflow: { |
| 181 | url: '/api/openai/siliconflow/models/embedding', |
| 182 | settingsKey: 'siliconflow_model', |
| 183 | selectId: 'vectors_siliconflow_model', |
| 184 | getBody: () => ({ siliconflow_endpoint: oai_settings.siliconflow_endpoint }), |
| 185 | }, |
| 186 | workers_ai: { |
| 187 | url: '/api/openai/workers-ai/models/embedding', |
| 188 | settingsKey: 'workers_ai_model', |
| 189 | selectId: 'vectors_workers_ai_model', |
| 190 | getBody: () => ({ workers_ai_account_id: oai_settings.workers_ai_account_id }), |
| 191 | }, |
| 192 | }; |
| 193 | |
| 194 | /** |
| 195 | * Gets the Collection ID for a file embedded in the chat. |
| 196 | * @param {string} fileUrl URL of the file |
| 197 | * @returns {string} Collection ID |
| 198 | */ |
| 199 | function getFileCollectionId(fileUrl) { |
| 200 | return `file_${getStringHash(fileUrl)}`; |
| 201 | } |
| 202 | |
| 203 | async function onVectorizeAllClick() { |
| 204 | try { |
| 205 | if (!settings.enabled_chats) { |
| 206 | return; |
| 207 | } |
| 208 | |
| 209 | const chatId = getCurrentChatId(); |
| 210 | |
| 211 | if (!chatId) { |
| 212 | toastr.info('No chat selected', 'Vectorization aborted'); |
| 213 | return; |
| 214 | } |
| 215 | |
| 216 | // Clear all cached summaries to ensure that new ones are created |
| 217 | // upon request of a full vectorise |
| 218 | cachedSummaries.clear(); |
| 219 | skippedHashes.clear(); |
| 220 | |
| 221 | const batchSize = getBatchSize(); |
| 222 | const elapsedLog = []; |
| 223 | let finished = false; |
| 224 | let initialPending = null; // total items pending at the start of this run — set on first sync return |
| 225 | $('#vectorize_progress').show(); |
| 226 | $('#vectorize_progress_percent').text('0'); |
| 227 | $('#vectorize_progress_eta').text('...'); |
| 228 | |
| 229 | while (!finished) { |
| 230 | if (is_send_press) { |
| 231 | toastr.info('Message generation is in progress.', 'Vectorization aborted'); |
| 232 | throw new Error('Message generation is in progress.'); |
| 233 | } |
| 234 | |
| 235 | const startTime = Date.now(); |
| 236 | const remaining = await synchronizeChat(batchSize); |
| 237 | const elapsed = Date.now() - startTime; |
| 238 | |
| 239 | if (remaining === null) { |
| 240 | // synchronizeChat already surfaced a toast; bail out of the loop. |
| 241 | throw new Error('Vectorization aborted'); |
| 242 | } |
| 243 | |
| 244 | elapsedLog.push(elapsed); |
| 245 | finished = remaining <= 0; |
| 246 | |
| 247 | if (initialPending === null) { |
| 248 | initialPending = Math.max(0, remaining + batchSize); |
| 249 | } |
| 250 | const pending = Math.max(0, remaining); |
| 251 | const processed = Math.max(0, initialPending - pending); |
| 252 | const processedPercent = initialPending > 0 |
| 253 | ? Math.min(100, Math.round((processed / initialPending) * 100)) |
| 254 | : 100; |
| 255 | const lastElapsed = elapsedLog.slice(-5); // last 5 elapsed times |
| 256 | const averageElapsed = lastElapsed.reduce((a, b) => a + b, 0) / lastElapsed.length; // average time needed to process one item |
| 257 | const pace = averageElapsed / batchSize; // time needed to process one item |
| 258 | const remainingTime = Math.round(pace * pending / 1000); |
| 259 | |
| 260 | $('#vectorize_progress_percent').text(processedPercent); |
| 261 | $('#vectorize_progress_eta').text(remainingTime); |
| 262 | |
| 263 | if (chatId !== getCurrentChatId()) { |
| 264 | throw new Error('Chat changed'); |
| 265 | } |
| 266 | } |
| 267 | if (skippedHashes.size > 0) { |
| 268 | toastr.warning(`${skippedHashes.size} message(s) skipped due to errors. Click Vectorize All again to retry.`, 'Vectorization partial'); |
| 269 | } |
| 270 | } catch (error) { |
| 271 | console.error('Vectors: Failed to vectorize all', error); |
| 272 | } finally { |
| 273 | $('#vectorize_progress').hide(); |
| 274 | } |
| 275 | } |
| 276 | |
| 277 | let syncBlocked = false; |
| 278 | |
| 279 | /** |
| 280 | * Gets the chunk delimiters for splitting text. |
| 281 | * @returns {string[]} Array of chunk delimiters |
| 282 | */ |
| 283 | function getChunkDelimiters() { |
| 284 | const delimiters = ['\n\n', '\n', ' ', '']; |
| 285 | |
| 286 | if (settings.force_chunk_delimiter) { |
| 287 | delimiters.unshift(settings.force_chunk_delimiter); |
| 288 | } |
| 289 | |
| 290 | return delimiters; |
| 291 | } |
| 292 | |
| 293 | /** |
| 294 | * Splits messages into chunks before inserting them into the vector index. |
| 295 | * @param {object[]} items Array of vector items |
| 296 | * @returns {object[]} Array of vector items (possibly chunked) |
| 297 | */ |
| 298 | function splitByChunks(items) { |
| 299 | if (settings.message_chunk_size <= 0) { |
| 300 | return items; |
| 301 | } |
| 302 | |
| 303 | const chunkedItems = []; |
| 304 | |
| 305 | for (const item of items) { |
| 306 | const chunks = splitRecursive(item.text, settings.message_chunk_size, getChunkDelimiters()); |
| 307 | for (const chunk of chunks) { |
| 308 | const chunkedItem = { ...item, text: chunk }; |
| 309 | chunkedItems.push(chunkedItem); |
| 310 | } |
| 311 | } |
| 312 | |
| 313 | return chunkedItems; |
| 314 | } |
| 315 | |
| 316 | /** |
| 317 | * Summarizes messages using the Extras API method. |
| 318 | * @param {HashedMessage} element hashed message |
| 319 | * @returns {Promise<boolean>} Sucess |
| 320 | */ |
| 321 | async function summarizeExtra(element) { |
| 322 | try { |
| 323 | const url = new URL(getApiUrl()); |
| 324 | url.pathname = '/api/summarize'; |
| 325 | |
| 326 | const apiResult = await doExtrasFetch(url, { |
| 327 | method: 'POST', |
| 328 | headers: { |
| 329 | 'Content-Type': 'application/json', |
| 330 | 'Bypass-Tunnel-Reminder': 'bypass', |
| 331 | }, |
| 332 | body: JSON.stringify({ |
| 333 | text: element.text, |
| 334 | params: {}, |
| 335 | }), |
| 336 | }); |
| 337 | |
| 338 | if (apiResult.ok) { |
| 339 | const data = await apiResult.json(); |
| 340 | element.text = removeReasoningFromString(data.summary); |
| 341 | } |
| 342 | } catch (error) { |
| 343 | console.log(error); |
| 344 | return false; |
| 345 | } |
| 346 | |
| 347 | return true; |
| 348 | } |
| 349 | |
| 350 | /** |
| 351 | * Summarizes messages using the main API method. |
| 352 | * @param {HashedMessage} element hashed message |
| 353 | * @returns {Promise<boolean>} Success |
| 354 | */ |
| 355 | async function summarizeMain(element) { |
| 356 | element.text = removeReasoningFromString(await generateRaw({ prompt: element.text, systemPrompt: settings.summary_prompt })); |
| 357 | return true; |
| 358 | } |
| 359 | |
| 360 | /** |
| 361 | * Summarizes messages using WebLLM. |
| 362 | * @param {HashedMessage} element hashed message |
| 363 | * @returns {Promise<boolean>} Success |
| 364 | */ |
| 365 | async function summarizeWebLLM(element) { |
| 366 | if (!isWebLlmSupported()) { |
| 367 | console.warn('Vectors: WebLLM is not supported'); |
| 368 | return false; |
| 369 | } |
| 370 | |
| 371 | const messages = [{ role: 'system', content: settings.summary_prompt }, { role: 'user', content: element.text }]; |
| 372 | element.text = removeReasoningFromString(await generateWebLlmChatPrompt(messages)); |
| 373 | |
| 374 | return true; |
| 375 | } |
| 376 | |
| 377 | /** |
| 378 | * Runs one summarization attempt for a single element via the chosen endpoint. |
| 379 | * @param {HashedMessage} element |
| 380 | * @param {string} endpoint |
| 381 | * @returns {Promise<boolean>} Whether the attempt succeeded. |
| 382 | */ |
| 383 | async function summarizeOne(element, endpoint) { |
| 384 | switch (endpoint) { |
| 385 | case 'main': |
| 386 | return await summarizeMain(element); |
| 387 | case 'extras': |
| 388 | return await summarizeExtra(element); |
| 389 | case 'webllm': |
| 390 | return await summarizeWebLLM(element); |
| 391 | default: |
| 392 | throw new Error(`Unsupported summary endpoint: ${endpoint}`, { cause: 'summary_endpoint_invalid' }); |
| 393 | } |
| 394 | } |
| 395 | |
| 396 | /** |
| 397 | * Summarizes messages using the chosen method. Every returned element has been |
| 398 | * summarized (via live call or cache). Throws if any element fails after |
| 399 | * `settings.summary_retries` attempts. |
| 400 | * @param {HashedMessage[]} hashedMessages Array of hashed messages (mutated in place) |
| 401 | * @param {string} endpoint Type of endpoint to use |
| 402 | * @param {Object} [options] Options for summarization behavior |
| 403 | * @param {boolean} [options.skipOnFailure=false] If true, tags failed elements with `summaryFailed = true` instead of throwing |
| 404 | * @returns {Promise<HashedMessage[]>} Summarized messages |
| 405 | */ |
| 406 | async function summarize(hashedMessages, endpoint = 'main', { skipOnFailure = false } = {}) { |
| 407 | const maxAttempts = Math.max(1, Number(settings.summary_retries) || 1); |
| 408 | for (const element of hashedMessages) { |
| 409 | const cachedSummary = cachedSummaries.get(element.hash); |
| 410 | if (cachedSummary) { |
| 411 | element.text = cachedSummary; |
| 412 | continue; |
| 413 | } |
| 414 | |
| 415 | let success = false; |
| 416 | for (let attempt = 1; attempt <= maxAttempts; attempt++) { |
| 417 | try { |
| 418 | success = await summarizeOne(element, endpoint); |
| 419 | if (success) break; |
| 420 | } catch (error) { |
| 421 | if (FATAL_CAUSES.has(error?.cause)) throw error; |
| 422 | console.warn(`Vectors: summary attempt ${attempt}/${maxAttempts} threw for hash ${element.hash}`, error); |
| 423 | } |
| 424 | console.warn(`Vectors: summary attempt ${attempt}/${maxAttempts} failed for hash ${element.hash}`); |
| 425 | } |
| 426 | if (!success) { |
| 427 | if (skipOnFailure) { |
| 428 | console.warn(`Vectors: summarization exhausted ${maxAttempts} attempt(s) for hash ${element.hash} — marking for skip`); |
| 429 | element.summaryFailed = true; |
| 430 | continue; |
| 431 | } |
| 432 | |
| 433 | throw new Error(`Summarization failed after ${maxAttempts} attempt(s)`, { cause: 'summary_failed' }); |
| 434 | } |
| 435 | cachedSummaries.set(element.hash, element.text); |
| 436 | } |
| 437 | return hashedMessages; |
| 438 | } |
| 439 | |
| 440 | async function synchronizeChat(batchSize = 5) { |
| 441 | if (!settings.enabled_chats) { |
| 442 | return -1; |
| 443 | } |
| 444 | |
| 445 | try { |
| 446 | await waitUntilCondition(() => !syncBlocked && !is_send_press, 1000); |
| 447 | } catch { |
| 448 | console.log('Vectors: Synchronization blocked by another process'); |
| 449 | return -1; |
| 450 | } |
| 451 | |
| 452 | try { |
| 453 | syncBlocked = true; |
| 454 | const context = getContext(); |
| 455 | const chatId = getCurrentChatId(); |
| 456 | |
| 457 | if (!chatId || !Array.isArray(context.chat)) { |
| 458 | console.debug('Vectors: No chat selected'); |
| 459 | return -1; |
| 460 | } |
| 461 | |
| 462 | /** @type {HashedMessage[]} */ |
| 463 | const hashedMessages = context.chat.filter(x => settings.keep_hidden || !x.is_system).map(x => ({ text: String(substituteParams(x.mes)), hash: getStringHash(substituteParams(x.mes)), index: context.chat.indexOf(x) })); |
| 464 | const hashesInCollection = await getSavedHashes(chatId); |
| 465 | |
| 466 | const newVectorItems = hashedMessages |
| 467 | .filter(x => !hashesInCollection.includes(x.hash)) |
| 468 | .filter(x => !skippedHashes.has(x.hash)); |
| 469 | const deletedHashes = hashesInCollection.filter(x => !hashedMessages.some(y => y.hash === x)); |
| 470 | |
| 471 | let batch = newVectorItems.slice(0, batchSize); |
| 472 | |
| 473 | if (settings.summarize) { |
| 474 | const minLength = Math.max(0, Number(settings.summary_threshold) || 0); |
| 475 | const toSummarize = minLength > 0 ? batch.filter(x => x.text.length >= minLength) : batch; |
| 476 | if (toSummarize.length > 0) { |
| 477 | await summarize(toSummarize, settings.summary_source, { skipOnFailure: true }); |
| 478 | const failed = toSummarize.filter(x => x.summaryFailed); |
| 479 | if (failed.length > 0) { |
| 480 | for (const item of failed) skippedHashes.add(item.hash); |
| 481 | batch = batch.filter(x => !x.summaryFailed); |
| 482 | } |
| 483 | } |
| 484 | } |
| 485 | |
| 486 | if (batch.length > 0) { |
| 487 | const chunkedBatch = splitByChunks(batch); |
| 488 | |
| 489 | console.log(`Vectors: Found ${newVectorItems.length} new items. Processing ${batch.length}...`); |
| 490 | try { |
| 491 | await insertVectorItems(chatId, chunkedBatch); |
| 492 | } catch (insertError) { |
| 493 | if (FATAL_CAUSES.has(insertError?.cause)) { |
| 494 | throw insertError; |
| 495 | } |
| 496 | console.warn('Vectors: insert failed for batch — marking for skip', insertError); |
| 497 | for (const item of batch) skippedHashes.add(item.hash); |
| 498 | } |
| 499 | } |
| 500 | |
| 501 | if (deletedHashes.length > 0) { |
| 502 | await deleteVectorItems(chatId, deletedHashes); |
| 503 | console.log(`Vectors: Deleted ${deletedHashes.length} old hashes`); |
| 504 | } |
| 505 | |
| 506 | return newVectorItems.length - batchSize; |
| 507 | } catch (error) { |
| 508 | /** |
| 509 | * Gets the error message for a given cause |
| 510 | * @param {string} cause Error cause key |
| 511 | * @returns {string} Error message |
| 512 | */ |
| 513 | function getErrorMessage(cause) { |
| 514 | switch (cause) { |
| 515 | case 'api_key_missing': |
| 516 | return 'API key missing. Save it in the "API Connections" panel.'; |
| 517 | case 'api_url_missing': |
| 518 | return 'API URL missing. Save it in the "API Connections" panel.'; |
| 519 | case 'api_model_missing': |
| 520 | return 'Vectorization Source Model is required, but not set.'; |
| 521 | case 'extras_module_missing': |
| 522 | return 'Extras API must provide an "embeddings" module.'; |
| 523 | case 'webllm_not_supported': |
| 524 | return 'WebLLM extension is not installed or the model is not set.'; |
| 525 | case 'account_id_missing': |
| 526 | return 'Workers AI account ID is required. Save it in the "API Connections" panel.'; |
| 527 | case 'summary_endpoint_invalid': |
| 528 | return 'Summarization endpoint is not supported.'; |
| 529 | case 'summary_failed': |
| 530 | return 'Summarization failed after the configured number of retries.'; |
| 531 | default: |
| 532 | return 'Check server console for more details'; |
| 533 | } |
| 534 | } |
| 535 | |
| 536 | console.error('Vectors: Failed to synchronize chat', error); |
| 537 | |
| 538 | const message = getErrorMessage(error.cause); |
| 539 | toastr.error(message, 'Vectorization failed', { preventDuplicates: true }); |
| 540 | return null; |
| 541 | } finally { |
| 542 | syncBlocked = false; |
| 543 | } |
| 544 | } |
| 545 | |
| 546 | /** |
| 547 | * @type {Map<string, number>} Cache object for storing hash values |
| 548 | */ |
| 549 | const hashCache = new Map(); |
| 550 | |
| 551 | /** |
| 552 | * Gets the hash value for a given string |
| 553 | * @param {string} str Input string |
| 554 | * @returns {number} Hash value |
| 555 | */ |
| 556 | function getStringHash(str) { |
| 557 | // Check if the hash is already in the cache |
| 558 | if (hashCache.has(str)) { |
| 559 | return hashCache.get(str); |
| 560 | } |
| 561 | |
| 562 | // Calculate the hash value |
| 563 | const hash = calculateHash(str); |
| 564 | |
| 565 | // Store the hash in the cache |
| 566 | hashCache.set(str, hash); |
| 567 | |
| 568 | return hash; |
| 569 | } |
| 570 | |
| 571 | /** |
| 572 | * Retrieves files from the chat and inserts them into the vector index. |
| 573 | * @param {ChatMessage[]} chat Array of chat messages |
| 574 | * @returns {Promise<void>} |
| 575 | */ |
| 576 | async function processFiles(chat) { |
| 577 | try { |
| 578 | if (!settings.enabled_files) { |
| 579 | return; |
| 580 | } |
| 581 | |
| 582 | const dataBankCollectionIds = await ingestDataBankAttachments(); |
| 583 | |
| 584 | if (dataBankCollectionIds.length) { |
| 585 | const queryText = await getQueryText(chat, 'file'); |
| 586 | await injectDataBankChunks(queryText, dataBankCollectionIds); |
| 587 | } |
| 588 | |
| 589 | for (const message of chat) { |
| 590 | // Message has no files |
| 591 | if (!Array.isArray(message?.extra?.files) || !message.extra.files.length) { |
| 592 | continue; |
| 593 | } |
| 594 | |
| 595 | // Trim file inserted by the script |
| 596 | const allFileText = String(message.mes || '').substring(0, message.extra.fileLength).trim(); |
| 597 | |
| 598 | // Convert kilobytes to string length |
| 599 | const thresholdLength = settings.size_threshold * 1024; |
| 600 | |
| 601 | // File is too small |
| 602 | if (allFileText.length < thresholdLength) { |
| 603 | continue; |
| 604 | } |
| 605 | |
| 606 | message.mes = message.mes.substring(message.extra.fileLength); |
| 607 | |
| 608 | const allFileChunks = []; |
| 609 | const queryText = await getQueryText(chat, 'file'); |
| 610 | |
| 611 | for (const file of message.extra.files) { |
| 612 | const fileName = file.name; |
| 613 | const fileUrl = file.url; |
| 614 | const collectionId = getFileCollectionId(fileUrl); |
| 615 | const hashesInCollection = await getSavedHashes(collectionId); |
| 616 | |
| 617 | // File is not vectorized yet |
| 618 | if (!hashesInCollection.length) { |
| 619 | const fileText = file.text || (await getFileAttachment(fileUrl)); |
| 620 | if (!fileText) { |
| 621 | continue; |
| 622 | } |
| 623 | await vectorizeFile(fileText, fileName, collectionId, settings.chunk_size, settings.overlap_percent); |
| 624 | } |
| 625 | |
| 626 | const fileChunks = await retrieveFileChunks(queryText, collectionId); |
| 627 | if (fileChunks) { |
| 628 | allFileChunks.push(fileChunks); |
| 629 | } |
| 630 | } |
| 631 | |
| 632 | message.mes = `${allFileChunks.join('\n\n')}\n\n${message.mes}`; |
| 633 | } |
| 634 | } catch (error) { |
| 635 | console.error('Vectors: Failed to retrieve files', error); |
| 636 | } |
| 637 | } |
| 638 | |
| 639 | /** |
| 640 | * Ensures that data bank attachments are ingested and inserted into the vector index. |
| 641 | * @param {string} [source] Optional source filter for data bank attachments. |
| 642 | * @returns {Promise<string[]>} Collection IDs |
| 643 | */ |
| 644 | async function ingestDataBankAttachments(source) { |
| 645 | // Exclude disabled files |
| 646 | const dataBank = source ? getDataBankAttachmentsForSource(source, false) : getDataBankAttachments(false); |
| 647 | const dataBankCollectionIds = []; |
| 648 | |
| 649 | for (const file of dataBank) { |
| 650 | const collectionId = getFileCollectionId(file.url); |
| 651 | const hashesInCollection = await getSavedHashes(collectionId); |
| 652 | dataBankCollectionIds.push(collectionId); |
| 653 | |
| 654 | // File is already in the collection |
| 655 | if (hashesInCollection.length) { |
| 656 | continue; |
| 657 | } |
| 658 | |
| 659 | // Download and process the file |
| 660 | const fileText = await getFileAttachment(file.url); |
| 661 | console.log(`Vectors: Retrieved file ${file.name} from Data Bank`); |
| 662 | // Convert kilobytes to string length |
| 663 | const thresholdLength = settings.size_threshold_db * 1024; |
| 664 | // Use chunk size from settings if file is larger than threshold |
| 665 | const chunkSize = file.size > thresholdLength ? settings.chunk_size_db : -1; |
| 666 | await vectorizeFile(fileText, file.name, collectionId, chunkSize, settings.overlap_percent_db); |
| 667 | } |
| 668 | |
| 669 | return dataBankCollectionIds; |
| 670 | } |
| 671 | |
| 672 | /** |
| 673 | * Inserts file chunks from the Data Bank into the prompt. |
| 674 | * @param {string} queryText Text to query |
| 675 | * @param {string[]} collectionIds File collection IDs |
| 676 | * @returns {Promise<void>} |
| 677 | */ |
| 678 | async function injectDataBankChunks(queryText, collectionIds) { |
| 679 | try { |
| 680 | const queryResults = await queryMultipleCollections(collectionIds, queryText, settings.chunk_count_db, settings.score_threshold); |
| 681 | console.debug(`Vectors: Retrieved ${collectionIds.length} Data Bank collections`, queryResults); |
| 682 | let textResult = ''; |
| 683 | |
| 684 | for (const collectionId in queryResults) { |
| 685 | console.debug(`Vectors: Processing Data Bank collection ${collectionId}`, queryResults[collectionId]); |
| 686 | const metadata = queryResults[collectionId].metadata?.filter(x => x.text)?.sort((a, b) => a.index - b.index)?.map(x => x.text)?.filter(onlyUnique) || []; |
| 687 | textResult += metadata.join('\n') + '\n\n'; |
| 688 | } |
| 689 | |
| 690 | if (!textResult) { |
| 691 | console.debug('Vectors: No Data Bank chunks found'); |
| 692 | return; |
| 693 | } |
| 694 | |
| 695 | const insertedText = substituteParamsExtended(settings.file_template_db, { text: textResult }); |
| 696 | setExtensionPrompt(EXTENSION_PROMPT_TAG_DB, insertedText, settings.file_position_db, settings.file_depth_db, settings.include_wi, settings.file_depth_role_db); |
| 697 | } catch (error) { |
| 698 | console.error('Vectors: Failed to insert Data Bank chunks', error); |
| 699 | } |
| 700 | } |
| 701 | |
| 702 | /** |
| 703 | * Retrieves file chunks from the vector index and inserts them into the chat. |
| 704 | * @param {string} queryText Text to query |
| 705 | * @param {string} collectionId File collection ID |
| 706 | * @returns {Promise<string>} Retrieved file text |
| 707 | */ |
| 708 | async function retrieveFileChunks(queryText, collectionId) { |
| 709 | console.debug(`Vectors: Retrieving file chunks for collection ${collectionId}`, queryText); |
| 710 | const queryResults = await queryCollection(collectionId, queryText, settings.chunk_count); |
| 711 | console.debug(`Vectors: Retrieved ${queryResults.hashes.length} file chunks for collection ${collectionId}`, queryResults); |
| 712 | const metadata = queryResults.metadata.filter(x => x.text).sort((a, b) => a.index - b.index).map(x => x.text).filter(onlyUnique); |
| 713 | const fileText = metadata.join('\n'); |
| 714 | |
| 715 | return fileText; |
| 716 | } |
| 717 | |
| 718 | /** |
| 719 | * Vectorizes a file and inserts it into the vector index. |
| 720 | * @param {string} fileText File text |
| 721 | * @param {string} fileName File name |
| 722 | * @param {string} collectionId File collection ID |
| 723 | * @param {number} chunkSize Chunk size |
| 724 | * @param {number} overlapPercent Overlap size (in %) |
| 725 | * @returns {Promise<boolean>} True if successful, false if not |
| 726 | */ |
| 727 | async function vectorizeFile(fileText, fileName, collectionId, chunkSize, overlapPercent) { |
| 728 | let toast = jQuery(); |
| 729 | |
| 730 | try { |
| 731 | if (settings.translate_files && typeof globalThis.translate === 'function') { |
| 732 | console.log(`Vectors: Translating file ${fileName} to English...`); |
| 733 | const translatedText = await globalThis.translate(fileText, 'en'); |
| 734 | fileText = translatedText; |
| 735 | } |
| 736 | |
| 737 | const batchSize = getBatchSize(); |
| 738 | const toastBody = $('<span>').text('This may take a while. Please wait...'); |
| 739 | toast = toastr.info(toastBody, `Ingesting file ${escapeHtml(fileName)}`, { closeButton: false, escapeHtml: false, timeOut: 0, extendedTimeOut: 0 }); |
| 740 | const overlapSize = Math.round(chunkSize * overlapPercent / 100); |
| 741 | const delimiters = getChunkDelimiters(); |
| 742 | // Overlap should not be included in chunk size. It will be later compensated by overlapChunks |
| 743 | chunkSize = overlapSize > 0 ? (chunkSize - overlapSize) : chunkSize; |
| 744 | const applyOverlap = (x, y, z) => overlapSize > 0 ? overlapChunks(x, y, z, overlapSize) : x; |
| 745 | const chunks = settings.only_custom_boundary && settings.force_chunk_delimiter |
| 746 | ? fileText.split(settings.force_chunk_delimiter).map(applyOverlap) |
| 747 | : splitRecursive(fileText, chunkSize, delimiters).map(applyOverlap); |
| 748 | console.debug(`Vectors: Split file ${fileName} into ${chunks.length} chunks with ${overlapPercent}% overlap`, chunks); |
| 749 | |
| 750 | const items = chunks.map((chunk, index) => ({ hash: getStringHash(chunk), text: chunk, index: index })); |
| 751 | |
| 752 | for (let i = 0; i < items.length; i += batchSize) { |
| 753 | toastBody.text(`${i}/${items.length} (${Math.round((i / items.length) * 100)}%) chunks processed`); |
| 754 | const chunkedBatch = items.slice(i, i + batchSize); |
| 755 | await insertVectorItems(collectionId, chunkedBatch); |
| 756 | } |
| 757 | |
| 758 | toastr.clear(toast); |
| 759 | console.log(`Vectors: Inserted ${chunks.length} vector items for file ${fileName} into ${collectionId}`); |
| 760 | return true; |
| 761 | } catch (error) { |
| 762 | toastr.clear(toast); |
| 763 | toastr.error(String(error), 'Failed to vectorize file', { preventDuplicates: true }); |
| 764 | console.error('Vectors: Failed to vectorize file', error); |
| 765 | return false; |
| 766 | } |
| 767 | } |
| 768 | |
| 769 | /** |
| 770 | * Removes the most relevant messages from the chat and displays them in the extension prompt |
| 771 | * @param {ChatMessage[]} chat Array of chat messages |
| 772 | * @param {number} _contextSize Context size (unused) |
| 773 | * @param {function} _abort Abort function (unused) |
| 774 | * @param {string} type Generation type |
| 775 | */ |
| 776 | async function rearrangeChat(chat, _contextSize, _abort, type) { |
| 777 | try { |
| 778 | if (type === 'quiet') { |
| 779 | console.debug('Vectors: Skipping quiet prompt'); |
| 780 | return; |
| 781 | } |
| 782 | |
| 783 | // Clear the extension prompt |
| 784 | setExtensionPrompt(EXTENSION_PROMPT_TAG, '', settings.position, settings.depth, settings.include_wi); |
| 785 | setExtensionPrompt(EXTENSION_PROMPT_TAG_DB, '', settings.file_position_db, settings.file_depth_db, settings.include_wi, settings.file_depth_role_db); |
| 786 | |
| 787 | if (settings.enabled_files) { |
| 788 | await processFiles(chat); |
| 789 | } |
| 790 | |
| 791 | if (settings.enabled_world_info) { |
| 792 | await activateWorldInfo(chat); |
| 793 | } |
| 794 | |
| 795 | if (!settings.enabled_chats) { |
| 796 | return; |
| 797 | } |
| 798 | |
| 799 | const chatId = getCurrentChatId(); |
| 800 | |
| 801 | if (!chatId || !Array.isArray(chat)) { |
| 802 | console.debug('Vectors: No chat selected'); |
| 803 | return; |
| 804 | } |
| 805 | |
| 806 | if (chat.length < settings.protect) { |
| 807 | console.debug(`Vectors: Not enough messages to rearrange (less than ${settings.protect})`); |
| 808 | return; |
| 809 | } |
| 810 | |
| 811 | const queryText = await getQueryText(chat, 'chat'); |
| 812 | |
| 813 | if (queryText.length === 0) { |
| 814 | console.debug('Vectors: No text to query'); |
| 815 | return; |
| 816 | } |
| 817 | |
| 818 | // Get the most relevant messages, excluding the last few |
| 819 | const queryResults = await queryCollection(chatId, queryText, settings.insert); |
| 820 | const queryHashes = queryResults.hashes.filter(onlyUnique); |
| 821 | const queriedMessages = []; |
| 822 | const insertedHashes = new Set(); |
| 823 | const retainMessages = chat.slice(-settings.protect); |
| 824 | |
| 825 | for (const message of chat) { |
| 826 | if (retainMessages.includes(message) || !message.mes) { |
| 827 | continue; |
| 828 | } |
| 829 | const hash = getStringHash(substituteParams(message.mes)); |
| 830 | if (queryHashes.includes(hash) && !insertedHashes.has(hash)) { |
| 831 | queriedMessages.push(message); |
| 832 | insertedHashes.add(hash); |
| 833 | } |
| 834 | } |
| 835 | |
| 836 | // Rearrange queried messages to match query order |
| 837 | // Order is reversed because more relevant are at the lower indices |
| 838 | queriedMessages.sort((a, b) => queryHashes.indexOf(getStringHash(substituteParams(b.mes))) - queryHashes.indexOf(getStringHash(substituteParams(a.mes)))); |
| 839 | |
| 840 | // Remove queried messages from the original chat array |
| 841 | for (const message of chat) { |
| 842 | if (queriedMessages.includes(message)) { |
| 843 | chat.splice(chat.indexOf(message), 1); |
| 844 | } |
| 845 | } |
| 846 | |
| 847 | if (queriedMessages.length === 0) { |
| 848 | console.debug('Vectors: No relevant messages found'); |
| 849 | return; |
| 850 | } |
| 851 | |
| 852 | // Format queried messages into a single string |
| 853 | const insertedText = getPromptText(queriedMessages); |
| 854 | setExtensionPrompt(EXTENSION_PROMPT_TAG, insertedText, settings.position, settings.depth, settings.include_wi); |
| 855 | } catch (error) { |
| 856 | toastr.error('Generation interceptor aborted. Check browser console for more details.', 'Vector Storage'); |
| 857 | console.error('Vectors: Failed to rearrange chat', error); |
| 858 | } |
| 859 | } |
| 860 | |
| 861 | /** |
| 862 | * @param {any[]} queriedMessages |
| 863 | * @returns {string} |
| 864 | */ |
| 865 | function getPromptText(queriedMessages) { |
| 866 | const queriedText = queriedMessages.map(x => collapseNewlines(`${x.name}: ${x.mes}`).trim()).join('\n\n'); |
| 867 | console.log('Vectors: relevant past messages found.\n', queriedText); |
| 868 | return substituteParamsExtended(settings.template, { text: queriedText }); |
| 869 | } |
| 870 | |
| 871 | /** |
| 872 | * Modifies text chunks to include overlap with adjacent chunks. |
| 873 | * @param {string} chunk Current item |
| 874 | * @param {number} index Current index |
| 875 | * @param {string[]} chunks List of chunks |
| 876 | * @param {number} overlapSize Size of the overlap |
| 877 | * @returns {string} Overlapped chunks, with overlap trimmed to sentence boundaries |
| 878 | */ |
| 879 | function overlapChunks(chunk, index, chunks, overlapSize) { |
| 880 | const halfOverlap = Math.floor(overlapSize / 2); |
| 881 | const nextChunk = chunks[index + 1]; |
| 882 | const prevChunk = chunks[index - 1]; |
| 883 | |
| 884 | const nextOverlap = trimToEndSentence(nextChunk?.substring(0, halfOverlap)) || ''; |
| 885 | const prevOverlap = trimToStartSentence(prevChunk?.substring(prevChunk.length - halfOverlap)) || ''; |
| 886 | const overlappedChunk = [prevOverlap, chunk, nextOverlap].filter(x => x).join(' '); |
| 887 | |
| 888 | return overlappedChunk; |
| 889 | } |
| 890 | |
| 891 | globalThis.vectors_rearrangeChat = rearrangeChat; |
| 892 | |
| 893 | const onChatEvent = debounce(async () => await moduleWorker.update(), debounce_timeout.relaxed); |
| 894 | |
| 895 | /** |
| 896 | * Gets the text to query from the chat |
| 897 | * @param {ChatMessage[]} chat Chat messages |
| 898 | * @param {'file'|'chat'|'world-info'} initiator Initiator of the query |
| 899 | * @returns {Promise<string>} Text to query |
| 900 | */ |
| 901 | async function getQueryText(chat, initiator) { |
| 902 | const getTextWithoutAttachments = (x) => { |
| 903 | const fileLength = x?.extra?.fileLength || 0; |
| 904 | return String(x?.mes || '').substring(fileLength).trim(); |
| 905 | }; |
| 906 | |
| 907 | let hashedMessages = chat |
| 908 | .map(x => ({ text: substituteParams(getTextWithoutAttachments(x)), hash: getStringHash(substituteParams(getTextWithoutAttachments(x))), index: chat.indexOf(x) })) |
| 909 | .filter(x => x.text) |
| 910 | .reverse() |
| 911 | .slice(0, settings.query); |
| 912 | |
| 913 | if (initiator === 'chat' && settings.enabled_chats && settings.summarize && settings.summarize_sent) { |
| 914 | const minLength = Math.max(0, Number(settings.summary_threshold) || 0); |
| 915 | const toSummarize = minLength > 0 ? hashedMessages.filter(x => x.text.length >= minLength) : hashedMessages; |
| 916 | if (toSummarize.length > 0) { |
| 917 | await summarize(toSummarize, settings.summary_source, { skipOnFailure: true }); |
| 918 | } |
| 919 | } |
| 920 | |
| 921 | const queryText = hashedMessages.map(x => x.text).join('\n'); |
| 922 | |
| 923 | return collapseNewlines(queryText).trim(); |
| 924 | } |
| 925 | |
| 926 | /** |
| 927 | * Gets common body parameters for vector requests. |
| 928 | * @param {object} args Additional arguments |
| 929 | * @returns {object} Request body |
| 930 | */ |
| 931 | function getVectorsRequestBody(args = {}) { |
| 932 | const body = Object.assign({}, args); |
| 933 | switch (settings.source) { |
| 934 | case 'extras': |
| 935 | body.extrasUrl = extension_settings.apiUrl; |
| 936 | body.extrasKey = extension_settings.apiKey; |
| 937 | break; |
| 938 | case 'electronhub': |
| 939 | body.model = extension_settings.vectors.electronhub_model; |
| 940 | break; |
| 941 | case 'openrouter': |
| 942 | body.model = extension_settings.vectors.openrouter_model; |
| 943 | break; |
| 944 | case 'togetherai': |
| 945 | body.model = extension_settings.vectors.togetherai_model; |
| 946 | break; |
| 947 | case 'openai': |
| 948 | body.model = extension_settings.vectors.openai_model; |
| 949 | break; |
| 950 | case 'cohere': |
| 951 | body.model = extension_settings.vectors.cohere_model; |
| 952 | break; |
| 953 | case 'ollama': |
| 954 | body.model = extension_settings.vectors.ollama_model; |
| 955 | body.apiUrl = settings.use_alt_endpoint ? settings.alt_endpoint_url : textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]; |
| 956 | body.keep = !!extension_settings.vectors.ollama_keep; |
| 957 | break; |
| 958 | case 'llamacpp': |
| 959 | body.apiUrl = settings.use_alt_endpoint ? settings.alt_endpoint_url : textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]; |
| 960 | break; |
| 961 | case 'vllm': |
| 962 | body.apiUrl = settings.use_alt_endpoint ? settings.alt_endpoint_url : textgenerationwebui_settings.server_urls[textgen_types.VLLM]; |
| 963 | body.model = extension_settings.vectors.vllm_model; |
| 964 | break; |
| 965 | case 'webllm': |
| 966 | body.model = extension_settings.vectors.webllm_model; |
| 967 | break; |
| 968 | case 'palm': |
| 969 | body.model = extension_settings.vectors.google_model; |
| 970 | body.api = 'makersuite'; |
| 971 | break; |
| 972 | case 'vertexai': |
| 973 | body.model = extension_settings.vectors.google_model; |
| 974 | body.api = 'vertexai'; |
| 975 | body.vertexai_auth_mode = oai_settings.vertexai_auth_mode; |
| 976 | body.vertexai_region = oai_settings.vertexai_region; |
| 977 | body.vertexai_express_project_id = oai_settings.vertexai_express_project_id; |
| 978 | break; |
| 979 | case 'chutes': |
| 980 | body.model = extension_settings.vectors.chutes_model; |
| 981 | break; |
| 982 | case 'nanogpt': |
| 983 | body.model = extension_settings.vectors.nanogpt_model; |
| 984 | break; |
| 985 | case 'siliconflow': |
| 986 | body.model = extension_settings.vectors.siliconflow_model; |
| 987 | body.siliconflow_endpoint = oai_settings.siliconflow_endpoint; |
| 988 | break; |
| 989 | case 'workers_ai': |
| 990 | body.model = extension_settings.vectors.workers_ai_model || '@cf/baai/bge-m3'; |
| 991 | body.workers_ai_account_id = oai_settings.workers_ai_account_id; |
| 992 | break; |
| 993 | default: |
| 994 | break; |
| 995 | } |
| 996 | return body; |
| 997 | } |
| 998 | |
| 999 | /** |
| 1000 | * Gets additional arguments for vector requests. |
| 1001 | * @param {string[]} items Items to embed |
| 1002 | * @returns {Promise<object>} Additional arguments |
| 1003 | */ |
| 1004 | async function getAdditionalArgs(items) { |
| 1005 | const args = {}; |
| 1006 | switch (settings.source) { |
| 1007 | case 'webllm': |
| 1008 | args.embeddings = await createWebLlmEmbeddings(items); |
| 1009 | break; |
| 1010 | case 'koboldcpp': { |
| 1011 | const { embeddings, model } = await createKoboldCppEmbeddings(items); |
| 1012 | args.embeddings = embeddings; |
| 1013 | args.model = model; |
| 1014 | break; |
| 1015 | } |
| 1016 | } |
| 1017 | return args; |
| 1018 | } |
| 1019 | |
| 1020 | /** |
| 1021 | * Gets the saved hashes for a collection |
| 1022 | * @param {string} collectionId |
| 1023 | * @returns {Promise<number[]>} Saved hashes |
| 1024 | */ |
| 1025 | async function getSavedHashes(collectionId) { |
| 1026 | const args = await getAdditionalArgs([]); |
| 1027 | const response = await fetch('/api/vector/list', { |
| 1028 | method: 'POST', |
| 1029 | headers: getRequestHeaders(), |
| 1030 | body: JSON.stringify({ |
| 1031 | ...getVectorsRequestBody(args), |
| 1032 | collectionId: collectionId, |
| 1033 | source: settings.source, |
| 1034 | }), |
| 1035 | }); |
| 1036 | |
| 1037 | if (!response.ok) { |
| 1038 | throw new Error(`Failed to get saved hashes for collection ${collectionId}`); |
| 1039 | } |
| 1040 | |
| 1041 | const hashes = await response.json(); |
| 1042 | return hashes; |
| 1043 | } |
| 1044 | |
| 1045 | /** |
| 1046 | * Inserts vector items into a collection |
| 1047 | * @param {string} collectionId - The collection to insert into |
| 1048 | * @param {{ hash: number, text: string }[]} items - The items to insert |
| 1049 | * @returns {Promise<void>} |
| 1050 | */ |
| 1051 | async function insertVectorItems(collectionId, items) { |
| 1052 | throwIfSourceInvalid(); |
| 1053 | |
| 1054 | const args = await getAdditionalArgs(items.map(x => x.text)); |
| 1055 | const response = await fetch('/api/vector/insert', { |
| 1056 | method: 'POST', |
| 1057 | headers: getRequestHeaders(), |
| 1058 | body: JSON.stringify({ |
| 1059 | ...getVectorsRequestBody(args), |
| 1060 | collectionId: collectionId, |
| 1061 | items: items, |
| 1062 | source: settings.source, |
| 1063 | }), |
| 1064 | }); |
| 1065 | |
| 1066 | if (!response.ok) { |
| 1067 | throw new Error(`Failed to insert vector items for collection ${collectionId}`); |
| 1068 | } |
| 1069 | } |
| 1070 | |
| 1071 | /** |
| 1072 | * Throws an error if the source is invalid (missing API key or URL, or missing module) |
| 1073 | */ |
| 1074 | function throwIfSourceInvalid() { |
| 1075 | if (settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI] || |
| 1076 | settings.source === 'electronhub' && !secret_state[SECRET_KEYS.ELECTRONHUB] || |
| 1077 | settings.source === 'chutes' && !secret_state[SECRET_KEYS.CHUTES] || |
| 1078 | settings.source === 'nanogpt' && !secret_state[SECRET_KEYS.NANOGPT] || |
| 1079 | settings.source === 'openrouter' && !secret_state[SECRET_KEYS.OPENROUTER] || |
| 1080 | settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] || |
| 1081 | settings.source === 'vertexai' && !secret_state[SECRET_KEYS.VERTEXAI] && !secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT] || |
| 1082 | settings.source === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] || |
| 1083 | settings.source === 'togetherai' && !secret_state[SECRET_KEYS.TOGETHERAI] || |
| 1084 | settings.source === 'nomicai' && !secret_state[SECRET_KEYS.NOMICAI] || |
| 1085 | settings.source === 'cohere' && !secret_state[SECRET_KEYS.COHERE] || |
| 1086 | settings.source === 'workers_ai' && !secret_state[SECRET_KEYS.WORKERS_AI] || |
| 1087 | settings.source === 'siliconflow' && !secret_state[SECRET_KEYS.SILICONFLOW]) { |
| 1088 | throw new Error('Vectors: API key missing', { cause: 'api_key_missing' }); |
| 1089 | } |
| 1090 | |
| 1091 | if (vectorApiRequiresUrl.includes(settings.source) && settings.use_alt_endpoint) { |
| 1092 | if (!settings.alt_endpoint_url) { |
| 1093 | throw new Error('Vectors: API URL missing', { cause: 'api_url_missing' }); |
| 1094 | } |
| 1095 | } else { |
| 1096 | if (settings.source === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA] || |
| 1097 | settings.source === 'vllm' && !textgenerationwebui_settings.server_urls[textgen_types.VLLM] || |
| 1098 | settings.source === 'koboldcpp' && !textgenerationwebui_settings.server_urls[textgen_types.KOBOLDCPP] || |
| 1099 | settings.source === 'llamacpp' && !textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) { |
| 1100 | throw new Error('Vectors: API URL missing', { cause: 'api_url_missing' }); |
| 1101 | } |
| 1102 | } |
| 1103 | |
| 1104 | if (settings.source === 'ollama' && !settings.ollama_model || settings.source === 'vllm' && !settings.vllm_model) { |
| 1105 | throw new Error('Vectors: API model missing', { cause: 'api_model_missing' }); |
| 1106 | } |
| 1107 | |
| 1108 | if (settings.source === 'extras' && !modules.includes('embeddings')) { |
| 1109 | throw new Error('Vectors: Embeddings module missing', { cause: 'extras_module_missing' }); |
| 1110 | } |
| 1111 | |
| 1112 | if (settings.source === 'webllm' && (!isWebLlmSupported() || !settings.webllm_model)) { |
| 1113 | throw new Error('Vectors: WebLLM is not supported', { cause: 'webllm_not_supported' }); |
| 1114 | } |
| 1115 | |
| 1116 | if (settings.source === 'workers_ai' && !oai_settings.workers_ai_account_id) { |
| 1117 | throw new Error('Vectors: Workers AI account ID missing', { cause: 'account_id_missing' }); |
| 1118 | } |
| 1119 | } |
| 1120 | |
| 1121 | /** |
| 1122 | * Deletes vector items from a collection |
| 1123 | * @param {string} collectionId - The collection to delete from |
| 1124 | * @param {number[]} hashes - The hashes of the items to delete |
| 1125 | * @returns {Promise<void>} |
| 1126 | */ |
| 1127 | async function deleteVectorItems(collectionId, hashes) { |
| 1128 | const args = await getAdditionalArgs([]); |
| 1129 | const response = await fetch('/api/vector/delete', { |
| 1130 | method: 'POST', |
| 1131 | headers: getRequestHeaders(), |
| 1132 | body: JSON.stringify({ |
| 1133 | ...getVectorsRequestBody(args), |
| 1134 | collectionId: collectionId, |
| 1135 | hashes: hashes, |
| 1136 | source: settings.source, |
| 1137 | }), |
| 1138 | }); |
| 1139 | |
| 1140 | if (!response.ok) { |
| 1141 | throw new Error(`Failed to delete vector items for collection ${collectionId}`); |
| 1142 | } |
| 1143 | } |
| 1144 | |
| 1145 | /** |
| 1146 | * @param {string} collectionId - The collection to query |
| 1147 | * @param {string} searchText - The text to query |
| 1148 | * @param {number} topK - The number of results to return |
| 1149 | * @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results |
| 1150 | */ |
| 1151 | async function queryCollection(collectionId, searchText, topK) { |
| 1152 | const args = await getAdditionalArgs([searchText]); |
| 1153 | const response = await fetch('/api/vector/query', { |
| 1154 | method: 'POST', |
| 1155 | headers: getRequestHeaders(), |
| 1156 | body: JSON.stringify({ |
| 1157 | ...getVectorsRequestBody(args), |
| 1158 | collectionId: collectionId, |
| 1159 | searchText: searchText, |
| 1160 | topK: topK, |
| 1161 | source: settings.source, |
| 1162 | threshold: settings.score_threshold, |
| 1163 | }), |
| 1164 | }); |
| 1165 | |
| 1166 | if (!response.ok) { |
| 1167 | throw new Error(`Failed to query collection ${collectionId}`); |
| 1168 | } |
| 1169 | |
| 1170 | return await response.json(); |
| 1171 | } |
| 1172 | |
| 1173 | /** |
| 1174 | * Queries multiple collections for a given text. |
| 1175 | * @param {string[]} collectionIds - Collection IDs to query |
| 1176 | * @param {string} searchText - Text to query |
| 1177 | * @param {number} topK - Number of results to return |
| 1178 | * @param {number} threshold - Score threshold |
| 1179 | * @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - Results mapped to collection IDs |
| 1180 | */ |
| 1181 | async function queryMultipleCollections(collectionIds, searchText, topK, threshold) { |
| 1182 | const args = await getAdditionalArgs([searchText]); |
| 1183 | const response = await fetch('/api/vector/query-multi', { |
| 1184 | method: 'POST', |
| 1185 | headers: getRequestHeaders(), |
| 1186 | body: JSON.stringify({ |
| 1187 | ...getVectorsRequestBody(args), |
| 1188 | collectionIds: collectionIds, |
| 1189 | searchText: searchText, |
| 1190 | topK: topK, |
| 1191 | source: settings.source, |
| 1192 | threshold: threshold ?? settings.score_threshold, |
| 1193 | }), |
| 1194 | }); |
| 1195 | |
| 1196 | if (!response.ok) { |
| 1197 | throw new Error('Failed to query multiple collections'); |
| 1198 | } |
| 1199 | |
| 1200 | return await response.json(); |
| 1201 | } |
| 1202 | |
| 1203 | /** |
| 1204 | * Purges the vector index for a file. |
| 1205 | * @param {string} fileUrl File URL to purge |
| 1206 | */ |
| 1207 | async function purgeFileVectorIndex(fileUrl) { |
| 1208 | try { |
| 1209 | if (!settings.enabled_files) { |
| 1210 | return; |
| 1211 | } |
| 1212 | |
| 1213 | console.log(`Vectors: Purging file vector index for ${fileUrl}`); |
| 1214 | const collectionId = getFileCollectionId(fileUrl); |
| 1215 | |
| 1216 | const response = await fetch('/api/vector/purge', { |
| 1217 | method: 'POST', |
| 1218 | headers: getRequestHeaders(), |
| 1219 | body: JSON.stringify({ |
| 1220 | ...getVectorsRequestBody(), |
| 1221 | collectionId: collectionId, |
| 1222 | }), |
| 1223 | }); |
| 1224 | |
| 1225 | if (!response.ok) { |
| 1226 | throw new Error(`Could not delete vector index for collection ${collectionId}`); |
| 1227 | } |
| 1228 | |
| 1229 | console.log(`Vectors: Purged vector index for collection ${collectionId}`); |
| 1230 | } catch (error) { |
| 1231 | console.error('Vectors: Failed to purge file', error); |
| 1232 | } |
| 1233 | } |
| 1234 | |
| 1235 | /** |
| 1236 | * Purges the vector index for a collection. |
| 1237 | * @param {string} collectionId Collection ID to purge |
| 1238 | * @returns <Promise<boolean>> True if deleted, false if not |
| 1239 | */ |
| 1240 | async function purgeVectorIndex(collectionId) { |
| 1241 | try { |
| 1242 | if (!settings.enabled_chats) { |
| 1243 | return true; |
| 1244 | } |
| 1245 | |
| 1246 | const response = await fetch('/api/vector/purge', { |
| 1247 | method: 'POST', |
| 1248 | headers: getRequestHeaders(), |
| 1249 | body: JSON.stringify({ |
| 1250 | ...getVectorsRequestBody(), |
| 1251 | collectionId: collectionId, |
| 1252 | }), |
| 1253 | }); |
| 1254 | |
| 1255 | if (!response.ok) { |
| 1256 | throw new Error(`Could not delete vector index for collection ${collectionId}`); |
| 1257 | } |
| 1258 | |
| 1259 | console.log(`Vectors: Purged vector index for collection ${collectionId}`); |
| 1260 | return true; |
| 1261 | } catch (error) { |
| 1262 | console.error('Vectors: Failed to purge', error); |
| 1263 | return false; |
| 1264 | } |
| 1265 | } |
| 1266 | |
| 1267 | /** |
| 1268 | * Purges all vector indexes. |
| 1269 | */ |
| 1270 | async function purgeAllVectorIndexes() { |
| 1271 | try { |
| 1272 | const response = await fetch('/api/vector/purge-all', { |
| 1273 | method: 'POST', |
| 1274 | headers: getRequestHeaders(), |
| 1275 | body: JSON.stringify({ |
| 1276 | ...getVectorsRequestBody(), |
| 1277 | }), |
| 1278 | }); |
| 1279 | |
| 1280 | if (!response.ok) { |
| 1281 | throw new Error('Failed to purge all vector indexes'); |
| 1282 | } |
| 1283 | |
| 1284 | console.log('Vectors: Purged all vector indexes'); |
| 1285 | toastr.success('All vector indexes purged', 'Purge successful'); |
| 1286 | } catch (error) { |
| 1287 | console.error('Vectors: Failed to purge all', error); |
| 1288 | toastr.error('Failed to purge all vector indexes', 'Purge failed'); |
| 1289 | } |
| 1290 | } |
| 1291 | |
| 1292 | function toggleSettings() { |
| 1293 | $('#vectors_files_settings').toggle(!!settings.enabled_files); |
| 1294 | $('#vectors_chats_settings').toggle(!!settings.enabled_chats); |
| 1295 | $('#vectors_world_info_settings').toggle(!!settings.enabled_world_info); |
| 1296 | $('#together_vectorsModel').toggle(settings.source === 'togetherai'); |
| 1297 | $('#openai_vectorsModel').toggle(settings.source === 'openai'); |
| 1298 | $('#electronhub_vectorsModel').toggle(settings.source === 'electronhub'); |
| 1299 | $('#chutes_vectorsModel').toggle(settings.source === 'chutes'); |
| 1300 | $('#nanogpt_vectorsModel').toggle(settings.source === 'nanogpt'); |
| 1301 | $('#openrouter_vectorsModel').toggle(settings.source === 'openrouter'); |
| 1302 | $('#cohere_vectorsModel').toggle(settings.source === 'cohere'); |
| 1303 | $('#ollama_vectorsModel').toggle(settings.source === 'ollama'); |
| 1304 | $('#llamacpp_vectorsModel').toggle(settings.source === 'llamacpp'); |
| 1305 | $('#vllm_vectorsModel').toggle(settings.source === 'vllm'); |
| 1306 | $('#nomicai_apiKey').toggle(settings.source === 'nomicai'); |
| 1307 | $('#webllm_vectorsModel').toggle(settings.source === 'webllm'); |
| 1308 | $('#koboldcpp_vectorsModel').toggle(settings.source === 'koboldcpp'); |
| 1309 | $('#google_vectorsModel').toggle(settings.source === 'palm' || settings.source === 'vertexai'); |
| 1310 | $('#siliconflow_vectorsModel').toggle(settings.source === 'siliconflow'); |
| 1311 | $('#workers_ai_vectorsModel').toggle(settings.source === 'workers_ai'); |
| 1312 | $('#vector_altEndpointUrl').toggle(vectorApiRequiresUrl.includes(settings.source)); |
| 1313 | if (settings.source === 'webllm') { |
| 1314 | loadWebLlmModels(); |
| 1315 | } else if (settings.source in remoteEmbeddingEndpoints) { |
| 1316 | loadRemoteEmbeddingModels(settings.source); |
| 1317 | } |
| 1318 | } |
| 1319 | |
| 1320 | /** |
| 1321 | * Loads models from a remote embedding endpoint and populates the corresponding select element. |
| 1322 | * @param {string} source - The source key matching a remoteEmbeddingEndpoints entry |
| 1323 | */ |
| 1324 | async function loadRemoteEmbeddingModels(source) { |
| 1325 | const config = remoteEmbeddingEndpoints[source]; |
| 1326 | if (!config) { |
| 1327 | return; |
| 1328 | } |
| 1329 | |
| 1330 | const { url, settingsKey, selectId, getBody, filter } = config; |
| 1331 | const valueProperty = config.valueProperty || 'id'; |
| 1332 | const textProperty = config.textProperty; |
| 1333 | |
| 1334 | /** |
| 1335 | * Populates the select element with the given models. |
| 1336 | * @param {any[]} models - Array of model objects |
| 1337 | */ |
| 1338 | function populateSelect(models) { |
| 1339 | const select = $(`#${selectId}`); |
| 1340 | select.empty(); |
| 1341 | for (const m of models) { |
| 1342 | const option = document.createElement('option'); |
| 1343 | option.value = m[valueProperty]; |
| 1344 | option.text = textProperty ? (m[textProperty] || m[valueProperty]) : m[valueProperty]; |
| 1345 | select.append(option); |
| 1346 | } |
| 1347 | if (!settings[settingsKey] && models.length) { |
| 1348 | settings[settingsKey] = models[0][valueProperty]; |
| 1349 | Object.assign(extension_settings.vectors, settings); |
| 1350 | saveSettingsDebounced(); |
| 1351 | } |
| 1352 | select.val(settings[settingsKey]); |
| 1353 | } |
| 1354 | |
| 1355 | try { |
| 1356 | const body = typeof getBody === 'function' ? getBody() : {}; |
| 1357 | |
| 1358 | /** @type {RequestInit} */ |
| 1359 | const fetchOptions = { |
| 1360 | method: 'POST', |
| 1361 | headers: getRequestHeaders(), |
| 1362 | body: JSON.stringify(body || {}), |
| 1363 | }; |
| 1364 | |
| 1365 | const response = await fetch(url, fetchOptions); |
| 1366 | if (!response.ok) { |
| 1367 | throw new Error(`HTTP ${response.status}`); |
| 1368 | } |
| 1369 | |
| 1370 | /** @type {Array<any>} */ |
| 1371 | const data = await response.json(); |
| 1372 | let models = Array.isArray(data) ? data : []; |
| 1373 | if (filter) { |
| 1374 | models = filter(models); |
| 1375 | } |
| 1376 | |
| 1377 | populateSelect(models); |
| 1378 | } catch (err) { |
| 1379 | console.warn(`${source} models fetch failed`, err); |
| 1380 | populateSelect([]); |
| 1381 | } |
| 1382 | } |
| 1383 | |
| 1384 | /** |
| 1385 | * Executes a function with WebLLM error handling. |
| 1386 | * @param {function(): Promise<T>} func Function to execute |
| 1387 | * @returns {Promise<T>} |
| 1388 | * @template T |
| 1389 | */ |
| 1390 | async function executeWithWebLlmErrorHandling(func) { |
| 1391 | try { |
| 1392 | return await func(); |
| 1393 | } catch (error) { |
| 1394 | console.log('Vectors: Failed to load WebLLM models', error); |
| 1395 | if (!(error instanceof Error)) { |
| 1396 | return; |
| 1397 | } |
| 1398 | switch (error.cause) { |
| 1399 | case 'webllm-not-available': |
| 1400 | toastr.warning('WebLLM is not available. Please install the extension.', 'WebLLM not installed'); |
| 1401 | break; |
| 1402 | case 'webllm-not-updated': |
| 1403 | toastr.warning('The installed extension version does not support embeddings.', 'WebLLM update required'); |
| 1404 | break; |
| 1405 | } |
| 1406 | } |
| 1407 | } |
| 1408 | |
| 1409 | /** |
| 1410 | * Loads and displays WebLLM models in the settings. |
| 1411 | * @returns {Promise<void>} |
| 1412 | */ |
| 1413 | function loadWebLlmModels() { |
| 1414 | return executeWithWebLlmErrorHandling(() => { |
| 1415 | const models = webllmProvider.getModels(); |
| 1416 | $('#vectors_webllm_model').empty(); |
| 1417 | for (const model of models) { |
| 1418 | $('#vectors_webllm_model').append($('<option>', { value: model.id, text: model.toString() })); |
| 1419 | } |
| 1420 | if (!settings.webllm_model || !models.some(x => x.id === settings.webllm_model)) { |
| 1421 | if (models.length) { |
| 1422 | settings.webllm_model = models[0].id; |
| 1423 | } |
| 1424 | } |
| 1425 | $('#vectors_webllm_model').val(settings.webllm_model); |
| 1426 | return Promise.resolve(); |
| 1427 | }); |
| 1428 | } |
| 1429 | |
| 1430 | /** |
| 1431 | * Creates WebLLM embeddings for a list of items. |
| 1432 | * @param {string[]} items Items to embed |
| 1433 | * @returns {Promise<Record<string, number[]>>} Calculated embeddings |
| 1434 | */ |
| 1435 | async function createWebLlmEmbeddings(items) { |
| 1436 | if (items.length === 0) { |
| 1437 | return /** @type {Record<string, number[]>} */ ({}); |
| 1438 | } |
| 1439 | return executeWithWebLlmErrorHandling(async () => { |
| 1440 | const embeddings = await webllmProvider.embedTexts(items, settings.webllm_model); |
| 1441 | const result = /** @type {Record<string, number[]>} */ ({}); |
| 1442 | for (let i = 0; i < items.length; i++) { |
| 1443 | result[items[i]] = embeddings[i]; |
| 1444 | } |
| 1445 | return result; |
| 1446 | }); |
| 1447 | } |
| 1448 | |
| 1449 | /** |
| 1450 | * Creates KoboldCpp embeddings for a list of items. |
| 1451 | * @param {string[]} items Items to embed |
| 1452 | * @returns {Promise<{embeddings: Record<string, number[]>, model: string}>} Calculated embeddings |
| 1453 | */ |
| 1454 | async function createKoboldCppEmbeddings(items) { |
| 1455 | const response = await fetch('/api/backends/kobold/embed', { |
| 1456 | method: 'POST', |
| 1457 | headers: getRequestHeaders(), |
| 1458 | body: JSON.stringify({ |
| 1459 | items: items, |
| 1460 | server: settings.use_alt_endpoint ? settings.alt_endpoint_url : textgenerationwebui_settings.server_urls[textgen_types.KOBOLDCPP], |
| 1461 | }), |
| 1462 | }); |
| 1463 | |
| 1464 | if (!response.ok) { |
| 1465 | throw new Error('Failed to get KoboldCpp embeddings'); |
| 1466 | } |
| 1467 | |
| 1468 | const data = await response.json(); |
| 1469 | if (!Array.isArray(data.embeddings) || !data.model || data.embeddings.length !== items.length) { |
| 1470 | throw new Error('Invalid response from KoboldCpp embeddings'); |
| 1471 | } |
| 1472 | |
| 1473 | const embeddings = /** @type {Record<string, number[]>} */ ({}); |
| 1474 | for (let i = 0; i < data.embeddings.length; i++) { |
| 1475 | if (!Array.isArray(data.embeddings[i]) || data.embeddings[i].length === 0) { |
| 1476 | throw new Error('KoboldCpp returned an empty embedding. Reduce the chunk size and/or size threshold and try again.'); |
| 1477 | } |
| 1478 | |
| 1479 | embeddings[items[i]] = data.embeddings[i]; |
| 1480 | } |
| 1481 | |
| 1482 | return { |
| 1483 | embeddings: embeddings, |
| 1484 | model: data.model, |
| 1485 | }; |
| 1486 | } |
| 1487 | |
| 1488 | async function onPurgeClick() { |
| 1489 | const chatId = getCurrentChatId(); |
| 1490 | if (!chatId) { |
| 1491 | toastr.info('No chat selected', 'Purge aborted'); |
| 1492 | return; |
| 1493 | } |
| 1494 | if (await purgeVectorIndex(chatId)) { |
| 1495 | toastr.success('Vector index purged', 'Purge successful'); |
| 1496 | } else { |
| 1497 | toastr.error('Failed to purge vector index', 'Purge failed'); |
| 1498 | } |
| 1499 | } |
| 1500 | |
| 1501 | async function onViewStatsClick() { |
| 1502 | const chatId = getCurrentChatId(); |
| 1503 | if (!chatId) { |
| 1504 | toastr.info('No chat selected'); |
| 1505 | return; |
| 1506 | } |
| 1507 | |
| 1508 | const hashesInCollection = await getSavedHashes(chatId); |
| 1509 | const totalHashes = hashesInCollection.length; |
| 1510 | const uniqueHashes = hashesInCollection.filter(onlyUnique).length; |
| 1511 | |
| 1512 | toastr.info(`Total hashes: <b>${totalHashes}</b><br> |
| 1513 | Unique hashes: <b>${uniqueHashes}</b><br><br> |
| 1514 | I'll mark collected messages with a green circle.`, |
| 1515 | `Stats for chat ${escapeHtml(chatId)}`, |
| 1516 | { timeOut: 10000, escapeHtml: false }, |
| 1517 | ); |
| 1518 | |
| 1519 | $('#chat .mes.vectorized').removeClass('vectorized'); |
| 1520 | const chat = getContext().chat; |
| 1521 | for (const message of chat) { |
| 1522 | if (hashesInCollection.includes(getStringHash(substituteParams(message.mes)))) { |
| 1523 | const messageElement = $(`#chat .mes[mesid="${chat.indexOf(message)}"]`); |
| 1524 | messageElement.addClass('vectorized'); |
| 1525 | } |
| 1526 | } |
| 1527 | } |
| 1528 | |
| 1529 | async function onVectorizeAllFilesClick() { |
| 1530 | try { |
| 1531 | const dataBank = getDataBankAttachments(); |
| 1532 | const chatAttachments = getContext().chat.filter(x => Array.isArray(x.extra?.files)).map(x => x.extra.files).flat(); |
| 1533 | const allFiles = [...dataBank, ...chatAttachments]; |
| 1534 | |
| 1535 | /** |
| 1536 | * Gets the chunk size for a file attachment. |
| 1537 | * @param file {import('../../chats.js').FileAttachment} File attachment |
| 1538 | * @returns {number} Chunk size for the file |
| 1539 | */ |
| 1540 | function getChunkSize(file) { |
| 1541 | if (chatAttachments.includes(file)) { |
| 1542 | // Convert kilobytes to string length |
| 1543 | const thresholdLength = settings.size_threshold * 1024; |
| 1544 | return file.size > thresholdLength ? settings.chunk_size : -1; |
| 1545 | } |
| 1546 | |
| 1547 | if (dataBank.includes(file)) { |
| 1548 | // Convert kilobytes to string length |
| 1549 | const thresholdLength = settings.size_threshold_db * 1024; |
| 1550 | // Use chunk size from settings if file is larger than threshold |
| 1551 | return file.size > thresholdLength ? settings.chunk_size_db : -1; |
| 1552 | } |
| 1553 | |
| 1554 | return -1; |
| 1555 | } |
| 1556 | |
| 1557 | /** |
| 1558 | * Gets the overlap percent for a file attachment. |
| 1559 | * @param file {import('../../chats.js').FileAttachment} File attachment |
| 1560 | * @returns {number} Overlap percent for the file |
| 1561 | */ |
| 1562 | function getOverlapPercent(file) { |
| 1563 | if (chatAttachments.includes(file)) { |
| 1564 | return settings.overlap_percent; |
| 1565 | } |
| 1566 | |
| 1567 | if (dataBank.includes(file)) { |
| 1568 | return settings.overlap_percent_db; |
| 1569 | } |
| 1570 | |
| 1571 | return 0; |
| 1572 | } |
| 1573 | |
| 1574 | let allSuccess = true; |
| 1575 | |
| 1576 | for (const file of allFiles) { |
| 1577 | const text = await getFileAttachment(file.url); |
| 1578 | const collectionId = getFileCollectionId(file.url); |
| 1579 | const hashes = await getSavedHashes(collectionId); |
| 1580 | |
| 1581 | if (hashes.length) { |
| 1582 | console.log(`Vectors: File ${file.name} is already vectorized`); |
| 1583 | continue; |
| 1584 | } |
| 1585 | |
| 1586 | const chunkSize = getChunkSize(file); |
| 1587 | const overlapPercent = getOverlapPercent(file); |
| 1588 | const result = await vectorizeFile(text, file.name, collectionId, chunkSize, overlapPercent); |
| 1589 | |
| 1590 | if (!result) { |
| 1591 | allSuccess = false; |
| 1592 | } |
| 1593 | } |
| 1594 | |
| 1595 | if (allSuccess) { |
| 1596 | toastr.success('All files vectorized', 'Vectorization successful'); |
| 1597 | } else { |
| 1598 | toastr.warning('Some files failed to vectorize. Check browser console for more details.', 'Vector Storage'); |
| 1599 | } |
| 1600 | } catch (error) { |
| 1601 | console.error('Vectors: Failed to vectorize all files', error); |
| 1602 | toastr.error('Failed to vectorize all files', 'Vectorization failed'); |
| 1603 | } |
| 1604 | } |
| 1605 | |
| 1606 | async function onPurgeFilesClick() { |
| 1607 | try { |
| 1608 | const dataBank = getDataBankAttachments(); |
| 1609 | const chatAttachments = getContext().chat.filter(x => Array.isArray(x.extra?.files)).map(x => x.extra.files).flat(); |
| 1610 | const allFiles = [...dataBank, ...chatAttachments]; |
| 1611 | |
| 1612 | for (const file of allFiles) { |
| 1613 | await purgeFileVectorIndex(file.url); |
| 1614 | } |
| 1615 | |
| 1616 | toastr.success('All files purged', 'Purge successful'); |
| 1617 | } catch (error) { |
| 1618 | console.error('Vectors: Failed to purge all files', error); |
| 1619 | toastr.error('Failed to purge all files', 'Purge failed'); |
| 1620 | } |
| 1621 | } |
| 1622 | |
| 1623 | async function activateWorldInfo(chat) { |
| 1624 | if (!settings.enabled_world_info) { |
| 1625 | console.debug('Vectors: Disabled for World Info'); |
| 1626 | return; |
| 1627 | } |
| 1628 | |
| 1629 | const entries = await getSortedEntries(); |
| 1630 | |
| 1631 | if (!Array.isArray(entries) || entries.length === 0) { |
| 1632 | console.debug('Vectors: No WI entries found'); |
| 1633 | return; |
| 1634 | } |
| 1635 | |
| 1636 | // Group entries by "world" field |
| 1637 | const groupedEntries = {}; |
| 1638 | |
| 1639 | for (const entry of entries) { |
| 1640 | // Skip orphaned entries. Is it even possible? |
| 1641 | if (!entry.world) { |
| 1642 | console.debug('Vectors: Skipped orphaned WI entry', entry); |
| 1643 | continue; |
| 1644 | } |
| 1645 | |
| 1646 | // Skip disabled entries |
| 1647 | if (entry.disable) { |
| 1648 | console.debug('Vectors: Skipped disabled WI entry', entry); |
| 1649 | continue; |
| 1650 | } |
| 1651 | |
| 1652 | // Skip entries without content |
| 1653 | if (!entry.content) { |
| 1654 | console.debug('Vectors: Skipped WI entry without content', entry); |
| 1655 | continue; |
| 1656 | } |
| 1657 | |
| 1658 | // Skip non-vectorized entries |
| 1659 | if (!entry.vectorized && !settings.enabled_for_all) { |
| 1660 | console.debug('Vectors: Skipped non-vectorized WI entry', entry); |
| 1661 | continue; |
| 1662 | } |
| 1663 | |
| 1664 | if (!Object.hasOwn(groupedEntries, entry.world)) { |
| 1665 | groupedEntries[entry.world] = []; |
| 1666 | } |
| 1667 | |
| 1668 | groupedEntries[entry.world].push(entry); |
| 1669 | } |
| 1670 | |
| 1671 | const collectionIds = []; |
| 1672 | |
| 1673 | if (Object.keys(groupedEntries).length === 0) { |
| 1674 | console.debug('Vectors: No WI entries to synchronize'); |
| 1675 | return; |
| 1676 | } |
| 1677 | |
| 1678 | // Synchronize collections |
| 1679 | for (const world in groupedEntries) { |
| 1680 | const collectionId = `world_${getStringHash(world)}`; |
| 1681 | const hashesInCollection = await getSavedHashes(collectionId); |
| 1682 | const newEntries = groupedEntries[world].filter(x => !hashesInCollection.includes(getStringHash(x.content))); |
| 1683 | const deletedHashes = hashesInCollection.filter(x => !groupedEntries[world].some(y => getStringHash(y.content) === x)); |
| 1684 | |
| 1685 | if (newEntries.length > 0) { |
| 1686 | console.log(`Vectors: Found ${newEntries.length} new WI entries for world ${world}`); |
| 1687 | await insertVectorItems(collectionId, newEntries.map(x => ({ hash: getStringHash(x.content), text: x.content, index: x.uid }))); |
| 1688 | } |
| 1689 | |
| 1690 | if (deletedHashes.length > 0) { |
| 1691 | console.log(`Vectors: Deleted ${deletedHashes.length} old hashes for world ${world}`); |
| 1692 | await deleteVectorItems(collectionId, deletedHashes); |
| 1693 | } |
| 1694 | |
| 1695 | collectionIds.push(collectionId); |
| 1696 | } |
| 1697 | |
| 1698 | // Perform a multi-query |
| 1699 | const queryText = await getQueryText(chat, 'world-info'); |
| 1700 | |
| 1701 | if (queryText.length === 0) { |
| 1702 | console.debug('Vectors: No text to query for WI'); |
| 1703 | return; |
| 1704 | } |
| 1705 | |
| 1706 | const queryResults = await queryMultipleCollections(collectionIds, queryText, settings.max_entries, settings.score_threshold); |
| 1707 | const activatedHashes = Object.values(queryResults).flatMap(x => x.hashes).filter(onlyUnique); |
| 1708 | const activatedEntries = []; |
| 1709 | |
| 1710 | // Activate entries found in the query results |
| 1711 | for (const entry of entries) { |
| 1712 | const hash = getStringHash(entry.content); |
| 1713 | |
| 1714 | if (activatedHashes.includes(hash)) { |
| 1715 | activatedEntries.push(entry); |
| 1716 | } |
| 1717 | } |
| 1718 | |
| 1719 | if (activatedEntries.length === 0) { |
| 1720 | console.debug('Vectors: No activated WI entries found'); |
| 1721 | return; |
| 1722 | } |
| 1723 | |
| 1724 | console.log(`Vectors: Activated ${activatedEntries.length} WI entries`, activatedEntries); |
| 1725 | await eventSource.emit(event_types.WORLDINFO_FORCE_ACTIVATE, activatedEntries); |
| 1726 | } |
| 1727 | |
| 1728 | export async function init() { |
| 1729 | if (!extension_settings.vectors) { |
| 1730 | extension_settings.vectors = settings; |
| 1731 | } |
| 1732 | |
| 1733 | // Migrate from old settings |
| 1734 | if (settings.enabled) { |
| 1735 | settings.enabled_chats = true; |
| 1736 | } |
| 1737 | |
| 1738 | Object.assign(settings, extension_settings.vectors); |
| 1739 | |
| 1740 | // Migrate from TensorFlow to Transformers |
| 1741 | settings.source = settings.source !== 'local' ? settings.source : 'transformers'; |
| 1742 | const template = await renderExtensionTemplateAsync(MODULE_NAME, 'settings'); |
| 1743 | $('#vectors_container').append(template); |
| 1744 | $('#vectors_enabled_chats').prop('checked', settings.enabled_chats).on('input', () => { |
| 1745 | settings.enabled_chats = $('#vectors_enabled_chats').prop('checked'); |
| 1746 | Object.assign(extension_settings.vectors, settings); |
| 1747 | saveSettingsDebounced(); |
| 1748 | toggleSettings(); |
| 1749 | }); |
| 1750 | $('#vectors_keep_hidden').prop('checked', settings.keep_hidden).on('input', () => { |
| 1751 | settings.keep_hidden = !!$('#vectors_keep_hidden').prop('checked'); |
| 1752 | Object.assign(extension_settings.vectors, settings); |
| 1753 | saveSettingsDebounced(); |
| 1754 | }); |
| 1755 | $('#vectors_enabled_files').prop('checked', settings.enabled_files).on('input', () => { |
| 1756 | settings.enabled_files = $('#vectors_enabled_files').prop('checked'); |
| 1757 | Object.assign(extension_settings.vectors, settings); |
| 1758 | saveSettingsDebounced(); |
| 1759 | toggleSettings(); |
| 1760 | }); |
| 1761 | $('#vectors_source').val(settings.source).on('change', () => { |
| 1762 | settings.source = String($('#vectors_source').val()); |
| 1763 | Object.assign(extension_settings.vectors, settings); |
| 1764 | saveSettingsDebounced(); |
| 1765 | toggleSettings(); |
| 1766 | }); |
| 1767 | $('#vector_altEndpointUrl_enabled').prop('checked', settings.use_alt_endpoint).on('input', () => { |
| 1768 | settings.use_alt_endpoint = $('#vector_altEndpointUrl_enabled').prop('checked'); |
| 1769 | Object.assign(extension_settings.vectors, settings); |
| 1770 | saveSettingsDebounced(); |
| 1771 | }); |
| 1772 | $('#vector_altEndpoint_address').val(settings.alt_endpoint_url).on('change', () => { |
| 1773 | settings.alt_endpoint_url = String($('#vector_altEndpoint_address').val()); |
| 1774 | Object.assign(extension_settings.vectors, settings); |
| 1775 | saveSettingsDebounced(); |
| 1776 | }); |
| 1777 | $('#vectors_togetherai_model').val(settings.togetherai_model).on('change', () => { |
| 1778 | settings.togetherai_model = String($('#vectors_togetherai_model').val()); |
| 1779 | Object.assign(extension_settings.vectors, settings); |
| 1780 | saveSettingsDebounced(); |
| 1781 | }); |
| 1782 | $('#vectors_openai_model').val(settings.openai_model).on('change', () => { |
| 1783 | settings.openai_model = String($('#vectors_openai_model').val()); |
| 1784 | Object.assign(extension_settings.vectors, settings); |
| 1785 | saveSettingsDebounced(); |
| 1786 | }); |
| 1787 | $('#vectors_electronhub_model').val(settings.electronhub_model).on('change', () => { |
| 1788 | settings.electronhub_model = String($('#vectors_electronhub_model').val()); |
| 1789 | Object.assign(extension_settings.vectors, settings); |
| 1790 | saveSettingsDebounced(); |
| 1791 | }); |
| 1792 | $('#vectors_chutes_model').val(settings.chutes_model).on('change', () => { |
| 1793 | settings.chutes_model = String($('#vectors_chutes_model').val()); |
| 1794 | Object.assign(extension_settings.vectors, settings); |
| 1795 | saveSettingsDebounced(); |
| 1796 | }); |
| 1797 | $('#vectors_nanogpt_model').val(settings.nanogpt_model).on('change', () => { |
| 1798 | settings.nanogpt_model = String($('#vectors_nanogpt_model').val()); |
| 1799 | Object.assign(extension_settings.vectors, settings); |
| 1800 | saveSettingsDebounced(); |
| 1801 | }); |
| 1802 | $('#vectors_siliconflow_model').val(settings.siliconflow_model).on('change', () => { |
| 1803 | settings.siliconflow_model = String($('#vectors_siliconflow_model').val()); |
| 1804 | Object.assign(extension_settings.vectors, settings); |
| 1805 | saveSettingsDebounced(); |
| 1806 | }); |
| 1807 | $('#vectors_workers_ai_model').val(settings.workers_ai_model).on('change', () => { |
| 1808 | settings.workers_ai_model = String($('#vectors_workers_ai_model').val()); |
| 1809 | Object.assign(extension_settings.vectors, settings); |
| 1810 | saveSettingsDebounced(); |
| 1811 | }); |
| 1812 | $('#vectors_openrouter_model').val(settings.openrouter_model).on('change', () => { |
| 1813 | settings.openrouter_model = String($('#vectors_openrouter_model').val()); |
| 1814 | Object.assign(extension_settings.vectors, settings); |
| 1815 | saveSettingsDebounced(); |
| 1816 | }); |
| 1817 | $('#vectors_cohere_model').val(settings.cohere_model).on('change', () => { |
| 1818 | settings.cohere_model = String($('#vectors_cohere_model').val()); |
| 1819 | Object.assign(extension_settings.vectors, settings); |
| 1820 | saveSettingsDebounced(); |
| 1821 | }); |
| 1822 | $('#vectors_ollama_model').val(settings.ollama_model).on('input', () => { |
| 1823 | settings.ollama_model = String($('#vectors_ollama_model').val()); |
| 1824 | Object.assign(extension_settings.vectors, settings); |
| 1825 | saveSettingsDebounced(); |
| 1826 | }); |
| 1827 | $('#vectors_vllm_model').val(settings.vllm_model).on('input', () => { |
| 1828 | settings.vllm_model = String($('#vectors_vllm_model').val()); |
| 1829 | Object.assign(extension_settings.vectors, settings); |
| 1830 | saveSettingsDebounced(); |
| 1831 | }); |
| 1832 | $('#vectors_ollama_keep').prop('checked', settings.ollama_keep).on('input', () => { |
| 1833 | settings.ollama_keep = $('#vectors_ollama_keep').prop('checked'); |
| 1834 | Object.assign(extension_settings.vectors, settings); |
| 1835 | saveSettingsDebounced(); |
| 1836 | }); |
| 1837 | $('#vectors_template').val(settings.template).on('input', () => { |
| 1838 | settings.template = String($('#vectors_template').val()); |
| 1839 | Object.assign(extension_settings.vectors, settings); |
| 1840 | saveSettingsDebounced(); |
| 1841 | }); |
| 1842 | $('#vectors_depth').val(settings.depth).on('input', () => { |
| 1843 | settings.depth = Number($('#vectors_depth').val()); |
| 1844 | Object.assign(extension_settings.vectors, settings); |
| 1845 | saveSettingsDebounced(); |
| 1846 | }); |
| 1847 | $('#vectors_protect').val(settings.protect).on('input', () => { |
| 1848 | settings.protect = Number($('#vectors_protect').val()); |
| 1849 | Object.assign(extension_settings.vectors, settings); |
| 1850 | saveSettingsDebounced(); |
| 1851 | }); |
| 1852 | $('#vectors_insert').val(settings.insert).on('input', () => { |
| 1853 | settings.insert = Number($('#vectors_insert').val()); |
| 1854 | Object.assign(extension_settings.vectors, settings); |
| 1855 | saveSettingsDebounced(); |
| 1856 | }); |
| 1857 | $('#vectors_query').val(settings.query).on('input', () => { |
| 1858 | settings.query = Number($('#vectors_query').val()); |
| 1859 | Object.assign(extension_settings.vectors, settings); |
| 1860 | saveSettingsDebounced(); |
| 1861 | }); |
| 1862 | $(`input[name="vectors_position"][value="${settings.position}"]`).prop('checked', true); |
| 1863 | $('input[name="vectors_position"]').on('change', () => { |
| 1864 | settings.position = Number($('input[name="vectors_position"]:checked').val()); |
| 1865 | Object.assign(extension_settings.vectors, settings); |
| 1866 | saveSettingsDebounced(); |
| 1867 | }); |
| 1868 | $('#vectors_vectorize_all').on('click', onVectorizeAllClick); |
| 1869 | $('#vectors_purge').on('click', onPurgeClick); |
| 1870 | $('#vectors_view_stats').on('click', onViewStatsClick); |
| 1871 | $('#vectors_files_vectorize_all').on('click', onVectorizeAllFilesClick); |
| 1872 | $('#vectors_files_purge').on('click', onPurgeFilesClick); |
| 1873 | |
| 1874 | $('#vectors_size_threshold').val(settings.size_threshold).on('input', () => { |
| 1875 | settings.size_threshold = Number($('#vectors_size_threshold').val()); |
| 1876 | Object.assign(extension_settings.vectors, settings); |
| 1877 | saveSettingsDebounced(); |
| 1878 | }); |
| 1879 | |
| 1880 | $('#vectors_chunk_size').val(settings.chunk_size).on('input', () => { |
| 1881 | settings.chunk_size = Number($('#vectors_chunk_size').val()); |
| 1882 | Object.assign(extension_settings.vectors, settings); |
| 1883 | saveSettingsDebounced(); |
| 1884 | }); |
| 1885 | |
| 1886 | $('#vectors_chunk_count').val(settings.chunk_count).on('input', () => { |
| 1887 | settings.chunk_count = Number($('#vectors_chunk_count').val()); |
| 1888 | Object.assign(extension_settings.vectors, settings); |
| 1889 | saveSettingsDebounced(); |
| 1890 | }); |
| 1891 | |
| 1892 | $('#vectors_include_wi').prop('checked', settings.include_wi).on('input', () => { |
| 1893 | settings.include_wi = !!$('#vectors_include_wi').prop('checked'); |
| 1894 | Object.assign(extension_settings.vectors, settings); |
| 1895 | saveSettingsDebounced(); |
| 1896 | }); |
| 1897 | |
| 1898 | $('#vectors_summarize').prop('checked', settings.summarize).on('input', () => { |
| 1899 | settings.summarize = !!$('#vectors_summarize').prop('checked'); |
| 1900 | Object.assign(extension_settings.vectors, settings); |
| 1901 | saveSettingsDebounced(); |
| 1902 | }); |
| 1903 | |
| 1904 | $('#vectors_summarize_user').prop('checked', settings.summarize_sent).on('input', () => { |
| 1905 | settings.summarize_sent = !!$('#vectors_summarize_user').prop('checked'); |
| 1906 | Object.assign(extension_settings.vectors, settings); |
| 1907 | saveSettingsDebounced(); |
| 1908 | }); |
| 1909 | |
| 1910 | $('#vectors_summary_source').val(settings.summary_source).on('change', () => { |
| 1911 | settings.summary_source = String($('#vectors_summary_source').val()); |
| 1912 | Object.assign(extension_settings.vectors, settings); |
| 1913 | saveSettingsDebounced(); |
| 1914 | }); |
| 1915 | |
| 1916 | $('#vectors_summary_prompt').val(settings.summary_prompt).on('input', () => { |
| 1917 | settings.summary_prompt = String($('#vectors_summary_prompt').val()); |
| 1918 | Object.assign(extension_settings.vectors, settings); |
| 1919 | saveSettingsDebounced(); |
| 1920 | }); |
| 1921 | |
| 1922 | $('#vectors_summary_retries').val(settings.summary_retries).on('input', () => { |
| 1923 | const parsed = Number($('#vectors_summary_retries').val()); |
| 1924 | settings.summary_retries = Number.isFinite(parsed) && parsed >= 1 ? Math.floor(parsed) : 1; |
| 1925 | Object.assign(extension_settings.vectors, settings); |
| 1926 | saveSettingsDebounced(); |
| 1927 | }); |
| 1928 | |
| 1929 | $('#vectors_summary_threshold').val(settings.summary_threshold).on('input', () => { |
| 1930 | const parsed = Number($('#vectors_summary_threshold').val()); |
| 1931 | settings.summary_threshold = Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 0; |
| 1932 | Object.assign(extension_settings.vectors, settings); |
| 1933 | saveSettingsDebounced(); |
| 1934 | }); |
| 1935 | |
| 1936 | $('#vectors_message_chunk_size').val(settings.message_chunk_size).on('input', () => { |
| 1937 | settings.message_chunk_size = Number($('#vectors_message_chunk_size').val()); |
| 1938 | Object.assign(extension_settings.vectors, settings); |
| 1939 | saveSettingsDebounced(); |
| 1940 | }); |
| 1941 | |
| 1942 | $('#vectors_size_threshold_db').val(settings.size_threshold_db).on('input', () => { |
| 1943 | settings.size_threshold_db = Number($('#vectors_size_threshold_db').val()); |
| 1944 | Object.assign(extension_settings.vectors, settings); |
| 1945 | saveSettingsDebounced(); |
| 1946 | }); |
| 1947 | |
| 1948 | $('#vectors_chunk_size_db').val(settings.chunk_size_db).on('input', () => { |
| 1949 | settings.chunk_size_db = Number($('#vectors_chunk_size_db').val()); |
| 1950 | Object.assign(extension_settings.vectors, settings); |
| 1951 | saveSettingsDebounced(); |
| 1952 | }); |
| 1953 | |
| 1954 | $('#vectors_chunk_count_db').val(settings.chunk_count_db).on('input', () => { |
| 1955 | settings.chunk_count_db = Number($('#vectors_chunk_count_db').val()); |
| 1956 | Object.assign(extension_settings.vectors, settings); |
| 1957 | saveSettingsDebounced(); |
| 1958 | }); |
| 1959 | |
| 1960 | $('#vectors_overlap_percent').val(settings.overlap_percent).on('input', () => { |
| 1961 | settings.overlap_percent = Number($('#vectors_overlap_percent').val()); |
| 1962 | Object.assign(extension_settings.vectors, settings); |
| 1963 | saveSettingsDebounced(); |
| 1964 | }); |
| 1965 | |
| 1966 | $('#vectors_overlap_percent_db').val(settings.overlap_percent_db).on('input', () => { |
| 1967 | settings.overlap_percent_db = Number($('#vectors_overlap_percent_db').val()); |
| 1968 | Object.assign(extension_settings.vectors, settings); |
| 1969 | saveSettingsDebounced(); |
| 1970 | }); |
| 1971 | |
| 1972 | $('#vectors_file_template_db').val(settings.file_template_db).on('input', () => { |
| 1973 | settings.file_template_db = String($('#vectors_file_template_db').val()); |
| 1974 | Object.assign(extension_settings.vectors, settings); |
| 1975 | saveSettingsDebounced(); |
| 1976 | }); |
| 1977 | |
| 1978 | $(`input[name="vectors_file_position_db"][value="${settings.file_position_db}"]`).prop('checked', true); |
| 1979 | $('input[name="vectors_file_position_db"]').on('change', () => { |
| 1980 | settings.file_position_db = Number($('input[name="vectors_file_position_db"]:checked').val()); |
| 1981 | Object.assign(extension_settings.vectors, settings); |
| 1982 | saveSettingsDebounced(); |
| 1983 | }); |
| 1984 | |
| 1985 | $('#vectors_file_depth_db').val(settings.file_depth_db).on('input', () => { |
| 1986 | settings.file_depth_db = Number($('#vectors_file_depth_db').val()); |
| 1987 | Object.assign(extension_settings.vectors, settings); |
| 1988 | saveSettingsDebounced(); |
| 1989 | }); |
| 1990 | |
| 1991 | $('#vectors_file_depth_role_db').val(settings.file_depth_role_db).on('input', () => { |
| 1992 | settings.file_depth_role_db = Number($('#vectors_file_depth_role_db').val()); |
| 1993 | Object.assign(extension_settings.vectors, settings); |
| 1994 | saveSettingsDebounced(); |
| 1995 | }); |
| 1996 | |
| 1997 | $('#vectors_translate_files').prop('checked', settings.translate_files).on('input', () => { |
| 1998 | settings.translate_files = !!$('#vectors_translate_files').prop('checked'); |
| 1999 | Object.assign(extension_settings.vectors, settings); |
| 2000 | saveSettingsDebounced(); |
| 2001 | }); |
| 2002 | |
| 2003 | $('#vectors_enabled_world_info').prop('checked', settings.enabled_world_info).on('input', () => { |
| 2004 | settings.enabled_world_info = !!$('#vectors_enabled_world_info').prop('checked'); |
| 2005 | Object.assign(extension_settings.vectors, settings); |
| 2006 | saveSettingsDebounced(); |
| 2007 | toggleSettings(); |
| 2008 | }); |
| 2009 | |
| 2010 | $('#vectors_enabled_for_all').prop('checked', settings.enabled_for_all).on('input', () => { |
| 2011 | settings.enabled_for_all = !!$('#vectors_enabled_for_all').prop('checked'); |
| 2012 | Object.assign(extension_settings.vectors, settings); |
| 2013 | saveSettingsDebounced(); |
| 2014 | }); |
| 2015 | |
| 2016 | $('#vectors_max_entries').val(settings.max_entries).on('input', () => { |
| 2017 | settings.max_entries = Number($('#vectors_max_entries').val()); |
| 2018 | Object.assign(extension_settings.vectors, settings); |
| 2019 | saveSettingsDebounced(); |
| 2020 | }); |
| 2021 | |
| 2022 | $('#vectors_score_threshold').val(settings.score_threshold).on('input', () => { |
| 2023 | settings.score_threshold = Number($('#vectors_score_threshold').val()); |
| 2024 | Object.assign(extension_settings.vectors, settings); |
| 2025 | saveSettingsDebounced(); |
| 2026 | }); |
| 2027 | |
| 2028 | $('#vectors_force_chunk_delimiter').val(settings.force_chunk_delimiter).on('input', () => { |
| 2029 | settings.force_chunk_delimiter = String($('#vectors_force_chunk_delimiter').val()); |
| 2030 | Object.assign(extension_settings.vectors, settings); |
| 2031 | saveSettingsDebounced(); |
| 2032 | }); |
| 2033 | |
| 2034 | $('#vectors_only_custom_boundary').prop('checked', settings.only_custom_boundary).on('input', () => { |
| 2035 | settings.only_custom_boundary = !!$('#vectors_only_custom_boundary').prop('checked'); |
| 2036 | Object.assign(extension_settings.vectors, settings); |
| 2037 | saveSettingsDebounced(); |
| 2038 | }); |
| 2039 | |
| 2040 | $('#vectors_ollama_pull').on('click', (e) => { |
| 2041 | const presetModel = extension_settings.vectors.ollama_model || ''; |
| 2042 | e.preventDefault(); |
| 2043 | $('#ollama_download_model').trigger('click'); |
| 2044 | $('#dialogue_popup_input').val(presetModel); |
| 2045 | }); |
| 2046 | |
| 2047 | $('#vectors_webllm_install').on('click', (e) => { |
| 2048 | e.preventDefault(); |
| 2049 | e.stopPropagation(); |
| 2050 | |
| 2051 | if (Object.hasOwn(SillyTavern, 'llm')) { |
| 2052 | toastr.info('WebLLM is already installed'); |
| 2053 | return; |
| 2054 | } |
| 2055 | |
| 2056 | openThirdPartyExtensionMenu('https://github.com/SillyTavern/Extension-WebLLM'); |
| 2057 | }); |
| 2058 | |
| 2059 | $('#vectors_webllm_model').on('input', () => { |
| 2060 | settings.webllm_model = String($('#vectors_webllm_model').val()); |
| 2061 | Object.assign(extension_settings.vectors, settings); |
| 2062 | saveSettingsDebounced(); |
| 2063 | }); |
| 2064 | |
| 2065 | $('#vectors_webllm_load').on('click', async () => { |
| 2066 | if (!settings.webllm_model) return; |
| 2067 | await webllmProvider.loadModel(settings.webllm_model); |
| 2068 | toastr.success('WebLLM model loaded'); |
| 2069 | }); |
| 2070 | |
| 2071 | $('#vectors_google_model').val(settings.google_model).on('input', () => { |
| 2072 | settings.google_model = String($('#vectors_google_model').val()); |
| 2073 | Object.assign(extension_settings.vectors, settings); |
| 2074 | saveSettingsDebounced(); |
| 2075 | }); |
| 2076 | |
| 2077 | $('#api_key_nomicai').toggleClass('success', !!secret_state[SECRET_KEYS.NOMICAI]); |
| 2078 | [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => { |
| 2079 | eventSource.on(event, (/** @type {string} */ key) => { |
| 2080 | if (key !== SECRET_KEYS.NOMICAI) return; |
| 2081 | $('#api_key_nomicai').toggleClass('success', !!secret_state[SECRET_KEYS.NOMICAI]); |
| 2082 | }); |
| 2083 | }); |
| 2084 | |
| 2085 | toggleSettings(); |
| 2086 | eventSource.on(event_types.MESSAGE_DELETED, onChatEvent); |
| 2087 | eventSource.on(event_types.MESSAGE_EDITED, onChatEvent); |
| 2088 | eventSource.on(event_types.MESSAGE_SENT, onChatEvent); |
| 2089 | eventSource.on(event_types.MESSAGE_RECEIVED, onChatEvent); |
| 2090 | eventSource.on(event_types.MESSAGE_SWIPED, onChatEvent); |
| 2091 | eventSource.on(event_types.CHAT_DELETED, purgeVectorIndex); |
| 2092 | eventSource.on(event_types.GROUP_CHAT_DELETED, purgeVectorIndex); |
| 2093 | eventSource.on(event_types.FILE_ATTACHMENT_DELETED, purgeFileVectorIndex); |
| 2094 | eventSource.on(event_types.EXTENSION_SETTINGS_LOADED, async (manifest) => { |
| 2095 | if (settings.source === 'webllm' && manifest?.display_name === 'WebLLM') { |
| 2096 | await loadWebLlmModels(); |
| 2097 | } |
| 2098 | }); |
| 2099 | |
| 2100 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2101 | name: 'db-ingest', |
| 2102 | callback: async () => { |
| 2103 | await ingestDataBankAttachments(); |
| 2104 | return ''; |
| 2105 | }, |
| 2106 | aliases: ['databank-ingest', 'data-bank-ingest'], |
| 2107 | helpString: 'Force the ingestion of all Data Bank attachments.', |
| 2108 | })); |
| 2109 | |
| 2110 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2111 | name: 'db-purge', |
| 2112 | callback: async () => { |
| 2113 | const dataBank = getDataBankAttachments(); |
| 2114 | |
| 2115 | for (const file of dataBank) { |
| 2116 | await purgeFileVectorIndex(file.url); |
| 2117 | } |
| 2118 | |
| 2119 | return ''; |
| 2120 | }, |
| 2121 | aliases: ['databank-purge', 'data-bank-purge'], |
| 2122 | helpString: 'Purge the vector index for all Data Bank attachments.', |
| 2123 | })); |
| 2124 | |
| 2125 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2126 | name: 'db-search', |
| 2127 | callback: async (args, query) => { |
| 2128 | const clamp = (v) => Number.isNaN(v) ? null : Math.min(1, Math.max(0, v)); |
| 2129 | const threshold = clamp(Number(args?.threshold ?? settings.score_threshold)); |
| 2130 | const validateCount = (v) => Number.isNaN(v) || !Number.isInteger(v) || v < 1 ? null : v; |
| 2131 | const count = validateCount(Number(args?.count)) ?? settings.chunk_count_db; |
| 2132 | const source = String(args?.source ?? ''); |
| 2133 | const attachments = source ? getDataBankAttachmentsForSource(source, false) : getDataBankAttachments(false); |
| 2134 | const collectionIds = await ingestDataBankAttachments(String(source)); |
| 2135 | const queryResults = await queryMultipleCollections(collectionIds, String(query), count, threshold); |
| 2136 | |
| 2137 | // Get URLs |
| 2138 | const urls = Object |
| 2139 | .keys(queryResults) |
| 2140 | .map(x => attachments.find(y => getFileCollectionId(y.url) === x)) |
| 2141 | .filter(x => x) |
| 2142 | .map(x => x.url); |
| 2143 | |
| 2144 | // Gets the actual text content of chunks |
| 2145 | const getChunksText = () => { |
| 2146 | let textResult = ''; |
| 2147 | for (const collectionId in queryResults) { |
| 2148 | const metadata = queryResults[collectionId].metadata?.filter(x => x.text)?.sort((a, b) => a.index - b.index)?.map(x => x.text)?.filter(onlyUnique) || []; |
| 2149 | textResult += metadata.join('\n') + '\n\n'; |
| 2150 | } |
| 2151 | return textResult; |
| 2152 | }; |
| 2153 | if (args.return === 'chunks') { |
| 2154 | return getChunksText(); |
| 2155 | } |
| 2156 | |
| 2157 | // @ts-ignore |
| 2158 | return slashCommandReturnHelper.doReturn(args.return ?? 'object', urls, { objectToStringFunc: list => list.join('\n') }); |
| 2159 | }, |
| 2160 | aliases: ['databank-search', 'data-bank-search'], |
| 2161 | helpString: 'Search the Data Bank for a specific query using vector similarity. Returns a list of file URLs with the most relevant content.', |
| 2162 | namedArgumentList: [ |
| 2163 | new SlashCommandNamedArgument('threshold', 'Threshold for the similarity score in the [0, 1] range. Uses the global config value if not set.', ARGUMENT_TYPE.NUMBER, false, false, ''), |
| 2164 | new SlashCommandNamedArgument('count', 'Maximum number of query results to return.', ARGUMENT_TYPE.NUMBER, false, false, ''), |
| 2165 | new SlashCommandNamedArgument('source', 'Optional filter for the attachments by source.', ARGUMENT_TYPE.STRING, false, false, '', ['global', 'character', 'chat']), |
| 2166 | SlashCommandNamedArgument.fromProps({ |
| 2167 | name: 'return', |
| 2168 | description: 'How you want the return value to be provided', |
| 2169 | typeList: [ARGUMENT_TYPE.STRING], |
| 2170 | defaultValue: 'object', |
| 2171 | enumList: [ |
| 2172 | new SlashCommandEnumValue('chunks', 'Return the actual content chunks', enumTypes.enum, '{}'), |
| 2173 | ...slashCommandReturnHelper.enumList({ allowObject: true }), |
| 2174 | ], |
| 2175 | forceEnum: true, |
| 2176 | }), |
| 2177 | ], |
| 2178 | unnamedArgumentList: [ |
| 2179 | new SlashCommandArgument('Query to search by.', ARGUMENT_TYPE.STRING, true, false), |
| 2180 | ], |
| 2181 | returns: ARGUMENT_TYPE.LIST, |
| 2182 | })); |
| 2183 | |
| 2184 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2185 | name: 'vector-threshold', |
| 2186 | helpString: 'Set the vector score threshold or return the current threshold if no argument is provided.', |
| 2187 | returns: 'score threshold value', |
| 2188 | unnamedArgumentList: [ |
| 2189 | SlashCommandArgument.fromProps({ |
| 2190 | description: 'Score threshold (number).', |
| 2191 | typeList: [ARGUMENT_TYPE.NUMBER], |
| 2192 | }), |
| 2193 | ], |
| 2194 | callback: async (_args, value) => { |
| 2195 | const raw = String(value ?? '').trim(); |
| 2196 | if (!raw) { |
| 2197 | return String(settings.score_threshold); |
| 2198 | } |
| 2199 | |
| 2200 | const parsed = Number(raw); |
| 2201 | if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) { |
| 2202 | toastr.warning('Score threshold must be a number between 0 and 1.'); |
| 2203 | return ''; |
| 2204 | } |
| 2205 | |
| 2206 | $('#vectors_score_threshold') |
| 2207 | .val(parsed) |
| 2208 | .trigger('input'); |
| 2209 | |
| 2210 | return String(settings.score_threshold); |
| 2211 | }, |
| 2212 | })); |
| 2213 | |
| 2214 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2215 | name: 'vector-query', |
| 2216 | helpString: 'Set the vector query messages or returns the current query messages count if no argument is provided', |
| 2217 | returns: 'the query messages value', |
| 2218 | unnamedArgumentList: [ |
| 2219 | SlashCommandArgument.fromProps({ |
| 2220 | description: 'Query messages (number > 0).', |
| 2221 | typeList: [ARGUMENT_TYPE.NUMBER], |
| 2222 | }), |
| 2223 | ], |
| 2224 | callback: async (_args, value) => { |
| 2225 | const raw = String(value ?? '').trim(); |
| 2226 | if (!raw) { |
| 2227 | return String(settings.query); |
| 2228 | } |
| 2229 | |
| 2230 | const parsed = Number(raw); |
| 2231 | if (!Number.isFinite(parsed) || parsed <= 0) { |
| 2232 | toastr.warning('Query messages must be a number greater than 0.'); |
| 2233 | return ''; |
| 2234 | } |
| 2235 | |
| 2236 | $('#vectors_query') |
| 2237 | .val(parsed) |
| 2238 | .trigger('input'); |
| 2239 | |
| 2240 | return String(settings.query); |
| 2241 | }, |
| 2242 | })); |
| 2243 | |
| 2244 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2245 | name: 'vector-max-entries', |
| 2246 | helpString: 'Set the vector world info max entries or returns the current max entries if no argument is provided', |
| 2247 | returns: 'world info max entries', |
| 2248 | unnamedArgumentList: [ |
| 2249 | SlashCommandArgument.fromProps({ |
| 2250 | description: 'Max entries (number > 0).', |
| 2251 | typeList: [ARGUMENT_TYPE.NUMBER], |
| 2252 | }), |
| 2253 | ], |
| 2254 | callback: async (_args, value) => { |
| 2255 | const raw = String(value ?? '').trim(); |
| 2256 | if (!raw) { |
| 2257 | return String(settings.max_entries); |
| 2258 | } |
| 2259 | |
| 2260 | const parsed = Number(raw); |
| 2261 | if (!Number.isFinite(parsed) || parsed <= 0) { |
| 2262 | toastr.warning('Max entries must be a number greater than 0.'); |
| 2263 | return ''; |
| 2264 | } |
| 2265 | |
| 2266 | $('#vectors_max_entries') |
| 2267 | .val(parsed) |
| 2268 | .trigger('input'); |
| 2269 | |
| 2270 | return String(settings.max_entries); |
| 2271 | }, |
| 2272 | })); |
| 2273 | |
| 2274 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2275 | name: 'vector-chats-state', |
| 2276 | helpString: 'Set whether chat vectorization is enabled or return the current boolean if no argument is provided', |
| 2277 | returns: 'boolean for if chat vectorization is enabled', |
| 2278 | unnamedArgumentList: [ |
| 2279 | SlashCommandArgument.fromProps({ |
| 2280 | description: 'boolean to set whether chat vectorization is enabled', |
| 2281 | typeList: [ARGUMENT_TYPE.BOOLEAN], |
| 2282 | enumList: commonEnumProviders.boolean('trueFalse')(), |
| 2283 | }), |
| 2284 | ], |
| 2285 | callback: async (_args, value) => { |
| 2286 | const raw = String(value ?? '').trim(); |
| 2287 | if (!raw) { |
| 2288 | return String(settings.enabled_chats); |
| 2289 | } |
| 2290 | |
| 2291 | const parsed = isTrueBoolean(raw); |
| 2292 | $('#vectors_enabled_chats') |
| 2293 | .prop('checked', parsed) |
| 2294 | .trigger('input'); |
| 2295 | |
| 2296 | return String(settings.enabled_chats); |
| 2297 | }, |
| 2298 | })); |
| 2299 | |
| 2300 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2301 | name: 'vector-files-state', |
| 2302 | helpString: 'Set whether file vectorization is enabled or return the current boolean if no argument is provided', |
| 2303 | returns: 'boolean for if file vectorization is enabled', |
| 2304 | unnamedArgumentList: [ |
| 2305 | SlashCommandArgument.fromProps({ |
| 2306 | description: 'boolean to set whether file vectorization is enabled', |
| 2307 | typeList: [ARGUMENT_TYPE.BOOLEAN], |
| 2308 | enumList: commonEnumProviders.boolean('trueFalse')(), |
| 2309 | }), |
| 2310 | ], |
| 2311 | callback: async (_args, value) => { |
| 2312 | const raw = String(value ?? '').trim(); |
| 2313 | if (!raw) { |
| 2314 | return String(settings.enabled_files); |
| 2315 | } |
| 2316 | |
| 2317 | const parsed = isTrueBoolean(raw) ; |
| 2318 | $('#vectors_enabled_files') |
| 2319 | .prop('checked', parsed) |
| 2320 | .trigger('input'); |
| 2321 | |
| 2322 | return String(settings.enabled_files); |
| 2323 | }, |
| 2324 | })); |
| 2325 | |
| 2326 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2327 | name: 'vector-worldinfo-state', |
| 2328 | helpString: 'Set whether world info vectorization is enabled or return the current boolean if no argument is provided', |
| 2329 | returns: 'boolean for if world info vectorization is enabled', |
| 2330 | unnamedArgumentList: [ |
| 2331 | SlashCommandArgument.fromProps({ |
| 2332 | description: 'boolean to set whether world info vectorization is enabled', |
| 2333 | typeList: [ARGUMENT_TYPE.BOOLEAN], |
| 2334 | enumList: commonEnumProviders.boolean('trueFalse')(), |
| 2335 | }), |
| 2336 | ], |
| 2337 | callback: async (_args, value) => { |
| 2338 | const raw = String(value ?? '').trim(); |
| 2339 | if (!raw) { |
| 2340 | return String(settings.enabled_world_info); |
| 2341 | } |
| 2342 | |
| 2343 | const parsed = isTrueBoolean(raw); |
| 2344 | $('#vectors_enabled_world_info') |
| 2345 | .prop('checked', parsed) |
| 2346 | .trigger('input'); |
| 2347 | |
| 2348 | return String(settings.enabled_world_info); |
| 2349 | }, |
| 2350 | })); |
| 2351 | |
| 2352 | registerDebugFunction('purge-everything', 'Purge all vector indices', 'Obliterate all stored vectors for all sources. No mercy.', async () => { |
| 2353 | if (!confirm('Are you sure?')) { |
| 2354 | return; |
| 2355 | } |
| 2356 | await purgeAllVectorIndexes(); |
| 2357 | }); |
| 2358 | } |