Blame Raw
Cohee · 51ad27fb · · 2358 lines (91.0 KB)
6 contributors
1import {
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';
15import {
16 ModuleWorkerWrapper,
17 extension_settings,
18 getContext,
19 modules,
20 renderExtensionTemplateAsync,
21 doExtrasFetch, getApiUrl,
22 openThirdPartyExtensionMenu,
23} from '../../extensions.js';
24import { collapseNewlines, registerDebugFunction } from '../../power-user.js';
25import { SECRET_KEYS, secret_state } from '../../secrets.js';
26import { getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment } from '../../chats.js';
27import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive, trimToStartSentence, trimToEndSentence, escapeHtml, isTrueBoolean } from '../../utils.js';
28import { debounce_timeout } from '../../constants.js';
29import { getSortedEntries } from '../../world-info.js';
30import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js';
31import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
32import { SlashCommand } from '../../slash-commands/SlashCommand.js';
33import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
34import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
35import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
36import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
37import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';
38import { WebLlmVectorProvider } from './webllm.js';
39import { removeReasoningFromString } from '../../reasoning.js';
40import { 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
50const MODULE_NAME = 'vectors';
51
52export const EXTENSION_PROMPT_TAG = '3_vectors';
53export const EXTENSION_PROMPT_TAG_DB = '4_vectors_data_bank';
54
55// Force solo chunks for sources that don't support batching.
56const getBatchSize = () => ['transformers', 'ollama'].includes(settings.source) ? 1 : 5;
57
58const 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
122const moduleWorker = new ModuleWorkerWrapper(synchronizeChat);
123const webllmProvider = new WebLlmVectorProvider();
124/**
125 * Cache for storing summaries of messages by their hash.
126 * @type {Map<number, string>}
127 */
128const 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 */
133const skippedHashes = new Set();
134/**
135 * Error causes treated as fatal — abort Vectorize All rather than skip.
136 * @type {Set<string>}
137 */
138const 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']);
139const 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>} */
153const 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 */
199function getFileCollectionId(fileUrl) {
200 return `file_${getStringHash(fileUrl)}`;
201}
202
203async 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
277let syncBlocked = false;
278
279/**
280 * Gets the chunk delimiters for splitting text.
281 * @returns {string[]} Array of chunk delimiters
282 */
283function 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 */
298function 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 */
321async 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 */
355async 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 */
365async 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 */
383async 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 */
406async 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
440async 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 */
549const 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 */
556function 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 */
576async 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 */
644async 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 */
678async 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 */
708async 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 */
727async 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 */
776async 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 */
865function 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 */
879function 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
891globalThis.vectors_rearrangeChat = rearrangeChat;
892
893const 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 */
901async 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 */
931function 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 */
1004async 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*/
1025async 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 */
1051async 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 */
1074function 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 */
1127async 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 */
1151async 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 */
1181async 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 */
1207async 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 */
1240async 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 */
1270async 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
1292function 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 */
1324async 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 */
1390async 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 */
1413function 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 */
1435async 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 */
1454async 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
1488async 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
1501async 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
1529async 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
1606async 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
1623async 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
1728export 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}