Enhance Vectorize All process with error handling, retries and minor improvements (#5479) * fix (vectors): Fixed Vectorize All progress report and ETA issues * fix (vectors): Added strip reasoning block function for extras/WebLLM summaries * feat(vectors): Retry failed summaries with configurable attempts * feat(vectors): Skip summarization for short messages * feat(vectors): Skip failed messages during Vectorize All instead of aborting all Prevents the "Vectorize All" process from stopping on single-message errors. Failed items are now skipped and reported at the end of the session rather than aborting the entire sync. Summarization: Implements per-message retries; failures use the original text as a fallback or mark for skipping. Vector Insertion: Differentiates fatal configuration errors (abort) from transient batch failures (skip and notify). * Resolved: 'account_id_missing' is missing * Resolved: Refactored out summarizeSkipOnFailure() functionality into summarize() via options parameter * Fix eslint and type checks * feat(vectors): add types to maps and sets, improve summarize function options --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

e5d4ff5fae3c7568512ddd67f1114a5f21c4376c

TanJeeSchuan <89920999+TanJeeSchuan@users.noreply.github.com>

Signed
2 files changed, +148 -36Showing whitespace changes
public/scripts/extensions/vectors/index.js+138 -36
@@ -44,6 +44,7 @@ import { oai_settings } from '../../openai.js';
44 * @property {string} text - The hashed message text44 * @property {string} text - The hashed message text
45 * @property {number} hash - The hash used as the vector key45 * @property {number} hash - The hash used as the vector key
46 * @property {number} index - The index of the message in the chat46 * @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)
47 */48 */
4849
49const MODULE_NAME = 'vectors';50const MODULE_NAME = 'vectors';
@@ -77,6 +78,8 @@ const settings = {
77 summarize_sent: false,78 summarize_sent: false,
78 summary_source: 'main',79 summary_source: 'main',
79 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.',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,
80 force_chunk_delimiter: '',83 force_chunk_delimiter: '',
8184
82 // For chats85 // For chats
@@ -118,7 +121,21 @@ const settings = {
118121
119const moduleWorker = new ModuleWorkerWrapper(synchronizeChat);122const moduleWorker = new ModuleWorkerWrapper(synchronizeChat);
120const webllmProvider = new WebLlmVectorProvider();123const webllmProvider = new WebLlmVectorProvider();
124/**
125 * Cache for storing summaries of messages by their hash.
126 * @type {Map<number, string>}
127 */
121const cachedSummaries = new Map();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']);
122const vectorApiRequiresUrl = ['llamacpp', 'vllm', 'ollama', 'koboldcpp'];139const vectorApiRequiresUrl = ['llamacpp', 'vllm', 'ollama', 'koboldcpp'];
123140
124/**141/**
@@ -199,10 +216,12 @@ async function onVectorizeAllClick() {
199 // Clear all cached summaries to ensure that new ones are created216 // Clear all cached summaries to ensure that new ones are created
200 // upon request of a full vectorise217 // upon request of a full vectorise
201 cachedSummaries.clear();218 cachedSummaries.clear();
219 skippedHashes.clear();
202220
203 const batchSize = getBatchSize();221 const batchSize = getBatchSize();
204 const elapsedLog = [];222 const elapsedLog = [];
205 let finished = false;223 let finished = false;
224 let initialPending = null; // total items pending at the start of this run — set on first sync return
206 $('#vectorize_progress').show();225 $('#vectorize_progress').show();
207 $('#vectorize_progress_percent').text('0');226 $('#vectorize_progress_percent').text('0');
208 $('#vectorize_progress_eta').text('...');227 $('#vectorize_progress_eta').text('...');
@@ -216,16 +235,27 @@ async function onVectorizeAllClick() {
216 const startTime = Date.now();235 const startTime = Date.now();
217 const remaining = await synchronizeChat(batchSize);236 const remaining = await synchronizeChat(batchSize);
218 const elapsed = Date.now() - startTime;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
219 elapsedLog.push(elapsed);244 elapsedLog.push(elapsed);
220 finished = remaining <= 0;245 finished = remaining <= 0;
221246
222 const total = getContext().chat.length;247 if (initialPending === null) {
223 const processed = total - remaining;248 initialPending = Math.max(0, remaining + batchSize);
224 const processedPercent = Math.round((processed / total) * 100); // percentage of the work done249 }
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;
225 const lastElapsed = elapsedLog.slice(-5); // last 5 elapsed times255 const lastElapsed = elapsedLog.slice(-5); // last 5 elapsed times
226 const averageElapsed = lastElapsed.reduce((a, b) => a + b, 0) / lastElapsed.length; // average time needed to process one item256 const averageElapsed = lastElapsed.reduce((a, b) => a + b, 0) / lastElapsed.length; // average time needed to process one item
227 const pace = averageElapsed / batchSize; // time needed to process one item257 const pace = averageElapsed / batchSize; // time needed to process one item
228 const remainingTime = Math.round(pace * remaining / 1000);258 const remainingTime = Math.round(pace * pending / 1000);
229259
230 $('#vectorize_progress_percent').text(processedPercent);260 $('#vectorize_progress_percent').text(processedPercent);
231 $('#vectorize_progress_eta').text(remainingTime);261 $('#vectorize_progress_eta').text(remainingTime);
@@ -234,6 +264,9 @@ async function onVectorizeAllClick() {
234 throw new Error('Chat changed');264 throw new Error('Chat changed');
235 }265 }
236 }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 }
237 } catch (error) {270 } catch (error) {
238 console.error('Vectors: Failed to vectorize all', error);271 console.error('Vectors: Failed to vectorize all', error);
239 } finally {272 } finally {
@@ -304,7 +337,7 @@ async function summarizeExtra(element) {
304337
305 if (apiResult.ok) {338 if (apiResult.ok) {
306 const data = await apiResult.json();339 const data = await apiResult.json();
307 element.text = data.summary;340 element.text = removeReasoningFromString(data.summary);
308 }341 }
309 } catch (error) {342 } catch (error) {
310 console.log(error);343 console.log(error);
@@ -336,45 +369,70 @@ async function summarizeWebLLM(element) {
336 }369 }
337370
338 const messages = [{ role: 'system', content: settings.summary_prompt }, { role: 'user', content: element.text }];371 const messages = [{ role: 'system', content: settings.summary_prompt }, { role: 'user', content: element.text }];
339 element.text = await generateWebLlmChatPrompt(messages);372 element.text = removeReasoningFromString(await generateWebLlmChatPrompt(messages));
340373
341 return true;374 return true;
342}375}
343376
344/**377/**
345 * Summarizes messages using the chosen method.378 * Runs one summarization attempt for a single element via the chosen endpoint.
346 * @param {HashedMessage[]} hashedMessages Array of hashed messages379 * @param {HashedMessage} element
347 * @param {string} endpoint Type of endpoint to use380 * @param {string} endpoint
348 * @returns {Promise<HashedMessage[]>} Summarized messages381 * @returns {Promise<boolean>} Whether the attempt succeeded.
349 */382 */
350async function summarize(hashedMessages, endpoint = 'main') {383async function summarizeOne(element, endpoint) {
351 for (const element of hashedMessages) {
352 const cachedSummary = cachedSummaries.get(element.hash);
353 if (!cachedSummary) {
354 let success = true;
355 switch (endpoint) {384 switch (endpoint) {
356 case 'main':385 case 'main':
357 success = await summarizeMain(element);386 return await summarizeMain(element);
358 break;
359 case 'extras':387 case 'extras':
360 success = await summarizeExtra(element);388 return await summarizeExtra(element);
361 break;
362 case 'webllm':389 case 'webllm':
363 success = await summarizeWebLLM(element);390 return await summarizeWebLLM(element);
364 break;
365 default:391 default:
366 console.error('Unsupported endpoint', endpoint);392 throw new Error(`Unsupported summary endpoint: ${endpoint}`, { cause: 'summary_endpoint_invalid' });
367 success = false;
368 break;
369 }393 }
370 if (success) {
371 cachedSummaries.set(element.hash, element.text);
372 } else {
373 break;
374}394}
375 } else {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) {
376 element.text = 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}`);
377 }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);
378 }436 }
379 return hashedMessages;437 return hashedMessages;
380}438}
@@ -401,21 +459,43 @@ async function synchronizeChat(batchSize = 5) {
401 return -1;459 return -1;
402 }460 }
403461
462 /** @type {HashedMessage[]} */
404 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) }));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) }));
405 const hashesInCollection = await getSavedHashes(chatId);464 const hashesInCollection = await getSavedHashes(chatId);
406465
407 let newVectorItems = hashedMessages.filter(x => !hashesInCollection.includes(x.hash));466 const newVectorItems = hashedMessages
467 .filter(x => !hashesInCollection.includes(x.hash))
468 .filter(x => !skippedHashes.has(x.hash));
408 const deletedHashes = hashesInCollection.filter(x => !hashedMessages.some(y => y.hash === x));469 const deletedHashes = hashesInCollection.filter(x => !hashedMessages.some(y => y.hash === x));
409470
471 let batch = newVectorItems.slice(0, batchSize);
472
410 if (settings.summarize) {473 if (settings.summarize) {
411 newVectorItems = await summarize(newVectorItems, settings.summary_source);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 }
412 }484 }
413485
414 if (newVectorItems.length > 0) {486 if (batch.length > 0) {
415 const chunkedBatch = splitByChunks(newVectorItems.slice(0, batchSize));487 const chunkedBatch = splitByChunks(batch);
416488
417 console.log(`Vectors: Found ${newVectorItems.length} new items. Processing ${batchSize}...`);489 console.log(`Vectors: Found ${newVectorItems.length} new items. Processing ${batch.length}...`);
490 try {
418 await insertVectorItems(chatId, chunkedBatch);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 }
419 }499 }
420500
421 if (deletedHashes.length > 0) {501 if (deletedHashes.length > 0) {
@@ -444,6 +524,10 @@ async function synchronizeChat(batchSize = 5) {
444 return 'WebLLM extension is not installed or the model is not set.';524 return 'WebLLM extension is not installed or the model is not set.';
445 case 'account_id_missing':525 case 'account_id_missing':
446 return 'Workers AI account ID is required. Save it in the "API Connections" panel.';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.';
447 default:531 default:
448 return 'Check server console for more details';532 return 'Check server console for more details';
449 }533 }
@@ -453,7 +537,7 @@ async function synchronizeChat(batchSize = 5) {
453537
454 const message = getErrorMessage(error.cause);538 const message = getErrorMessage(error.cause);
455 toastr.error(message, 'Vectorization failed', { preventDuplicates: true });539 toastr.error(message, 'Vectorization failed', { preventDuplicates: true });
456 return -1;540 return null;
457 } finally {541 } finally {
458 syncBlocked = false;542 syncBlocked = false;
459 }543 }
@@ -827,7 +911,11 @@ async function getQueryText(chat, initiator) {
827 .slice(0, settings.query);911 .slice(0, settings.query);
828912
829 if (initiator === 'chat' && settings.enabled_chats && settings.summarize && settings.summarize_sent) {913 if (initiator === 'chat' && settings.enabled_chats && settings.summarize && settings.summarize_sent) {
830 hashedMessages = await summarize(hashedMessages, settings.summary_source);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 }
831 }919 }
832920
833 const queryText = hashedMessages.map(x => x.text).join('\n');921 const queryText = hashedMessages.map(x => x.text).join('\n');
@@ -1830,6 +1918,20 @@ export async function init() {
1830 saveSettingsDebounced();1918 saveSettingsDebounced();
1831 });1919 });
18321920
1921 $('#vectors_summary_retries').val(settings.summary_retries).on('input', () => {
1922 const parsed = Number($('#vectors_summary_retries').val());
1923 settings.summary_retries = Number.isFinite(parsed) && parsed >= 1 ? Math.floor(parsed) : 1;
1924 Object.assign(extension_settings.vectors, settings);
1925 saveSettingsDebounced();
1926 });
1927
1928 $('#vectors_summary_threshold').val(settings.summary_threshold).on('input', () => {
1929 const parsed = Number($('#vectors_summary_threshold').val());
1930 settings.summary_threshold = Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 0;
1931 Object.assign(extension_settings.vectors, settings);
1932 saveSettingsDebounced();
1933 });
1934
1833 $('#vectors_message_chunk_size').val(settings.message_chunk_size).on('input', () => {1935 $('#vectors_message_chunk_size').val(settings.message_chunk_size).on('input', () => {
1834 settings.message_chunk_size = Number($('#vectors_message_chunk_size').val());1936 settings.message_chunk_size = Number($('#vectors_message_chunk_size').val());
1835 Object.assign(extension_settings.vectors, settings);1937 Object.assign(extension_settings.vectors, settings);
public/scripts/extensions/vectors/settings.html+10 -0
@@ -493,6 +493,16 @@
493 <label for="vectors_summary_prompt" title="Summary Prompt:">Summary Prompt:</label>493 <label for="vectors_summary_prompt" title="Summary Prompt:">Summary Prompt:</label>
494 <small data-i18n="Only used when Main API or WebLLM Extension is selected.">Only used when Main API or WebLLM Extension is selected.</small>494 <small data-i18n="Only used when Main API or WebLLM Extension is selected.">Only used when Main API or WebLLM Extension is selected.</small>
495 <textarea id="vectors_summary_prompt" class="text_pole textarea_compact" rows="6" placeholder="This prompt will be sent to AI to request the summary generation."></textarea>495 <textarea id="vectors_summary_prompt" class="text_pole textarea_compact" rows="6" placeholder="This prompt will be sent to AI to request the summary generation."></textarea>
496
497 <label for="vectors_summary_retries" title="Number of attempts per message before aborting vectorization.">
498 <span data-i18n="Summarization retries per message">Summarization retries per message</span>
499 </label>
500 <input id="vectors_summary_retries" type="number" class="text_pole widthUnset" min="1" max="10" step="1" />
501
502 <label for="vectors_summary_threshold" title="Messages shorter than this (in characters) are embedded as-is without summarization. Set to 0 to always summarize.">
503 <span data-i18n="Summarization min length (chars)">Summarization min length (chars)</span>
504 </label>
505 <input id="vectors_summary_threshold" type="number" class="text_pole widthUnset" min="0" step="1" />
496 </div>506 </div>
497 </div>507 </div>
498 <small data-i18n="Old messages are vectorized gradually as you chat. To process all previous messages, click the button below.">508 <small data-i18n="Old messages are vectorized gradually as you chat. To process all previous messages, click the button below.">