Add support for KoboldCpp embeddings in Vector Storage (#3795) * Add support for KoboldCpp embeddings in vector processing * Add validation for KoboldCpp embeddings to handle empty data * Improve toast handling

80e821d12dbca4827081e388a501a8e8ed4f9527

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
4 files changed, +113 -1Showing whitespace changes
public/scripts/extensions/vectors/index.js+51 -1
@@ -565,6 +565,8 @@ async function retrieveFileChunks(queryText, collectionId) {
565 * @returns {Promise<boolean>} True if successful, false if not565 * @returns {Promise<boolean>} True if successful, false if not
566 */566 */
567async function vectorizeFile(fileText, fileName, collectionId, chunkSize, overlapPercent) {567async function vectorizeFile(fileText, fileName, collectionId, chunkSize, overlapPercent) {
568 let toast = jQuery();
569
568 try {570 try {
569 if (settings.translate_files && typeof globalThis.translate === 'function') {571 if (settings.translate_files && typeof globalThis.translate === 'function') {
570 console.log(`Vectors: Translating file ${fileName} to English...`);572 console.log(`Vectors: Translating file ${fileName} to English...`);
@@ -574,7 +576,7 @@ async function vectorizeFile(fileText, fileName, collectionId, chunkSize, overla
574576
575 const batchSize = getBatchSize();577 const batchSize = getBatchSize();
576 const toastBody = $('<span>').text('This may take a while. Please wait...');578 const toastBody = $('<span>').text('This may take a while. Please wait...');
577 const toast = toastr.info(toastBody, `Ingesting file ${escapeHtml(fileName)}`, { closeButton: false, escapeHtml: false, timeOut: 0, extendedTimeOut: 0 });579 toast = toastr.info(toastBody, `Ingesting file ${escapeHtml(fileName)}`, { closeButton: false, escapeHtml: false, timeOut: 0, extendedTimeOut: 0 });
578 const overlapSize = Math.round(chunkSize * overlapPercent / 100);580 const overlapSize = Math.round(chunkSize * overlapPercent / 100);
579 const delimiters = getChunkDelimiters();581 const delimiters = getChunkDelimiters();
580 // Overlap should not be included in chunk size. It will be later compensated by overlapChunks582 // Overlap should not be included in chunk size. It will be later compensated by overlapChunks
@@ -596,6 +598,7 @@ async function vectorizeFile(fileText, fileName, collectionId, chunkSize, overla
596 console.log(`Vectors: Inserted ${chunks.length} vector items for file ${fileName} into ${collectionId}`);598 console.log(`Vectors: Inserted ${chunks.length} vector items for file ${fileName} into ${collectionId}`);
597 return true;599 return true;
598 } catch (error) {600 } catch (error) {
601 toastr.clear(toast);
599 toastr.error(String(error), 'Failed to vectorize file', { preventDuplicates: true });602 toastr.error(String(error), 'Failed to vectorize file', { preventDuplicates: true });
600 console.error('Vectors: Failed to vectorize file', error);603 console.error('Vectors: Failed to vectorize file', error);
601 return false;604 return false;
@@ -803,6 +806,12 @@ async function getAdditionalArgs(items) {
803 case 'webllm':806 case 'webllm':
804 args.embeddings = await createWebLlmEmbeddings(items);807 args.embeddings = await createWebLlmEmbeddings(items);
805 break;808 break;
809 case 'koboldcpp': {
810 const { embeddings, model } = await createKoboldCppEmbeddings(items);
811 args.embeddings = embeddings;
812 args.model = model;
813 break;
814 }
806 }815 }
807 return args;816 return args;
808}817}
@@ -872,6 +881,7 @@ function throwIfSourceInvalid() {
872881
873 if (settings.source === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA] ||882 if (settings.source === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA] ||
874 settings.source === 'vllm' && !textgenerationwebui_settings.server_urls[textgen_types.VLLM] ||883 settings.source === 'vllm' && !textgenerationwebui_settings.server_urls[textgen_types.VLLM] ||
884 settings.source === 'koboldcpp' && !textgenerationwebui_settings.server_urls[textgen_types.KOBOLDCPP] ||
875 settings.source === 'llamacpp' && !textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) {885 settings.source === 'llamacpp' && !textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) {
876 throw new Error('Vectors: API URL missing', { cause: 'api_url_missing' });886 throw new Error('Vectors: API URL missing', { cause: 'api_url_missing' });
877 }887 }
@@ -1071,6 +1081,7 @@ function toggleSettings() {
1071 $('#vllm_vectorsModel').toggle(settings.source === 'vllm');1081 $('#vllm_vectorsModel').toggle(settings.source === 'vllm');
1072 $('#nomicai_apiKey').toggle(settings.source === 'nomicai');1082 $('#nomicai_apiKey').toggle(settings.source === 'nomicai');
1073 $('#webllm_vectorsModel').toggle(settings.source === 'webllm');1083 $('#webllm_vectorsModel').toggle(settings.source === 'webllm');
1084 $('#koboldcpp_vectorsModel').toggle(settings.source === 'koboldcpp');
1074 if (settings.source === 'webllm') {1085 if (settings.source === 'webllm') {
1075 loadWebLlmModels();1086 loadWebLlmModels();
1076 }1087 }
@@ -1138,6 +1149,45 @@ async function createWebLlmEmbeddings(items) {
1138 });1149 });
1139}1150}
11401151
1152/**
1153 * Creates KoboldCpp embeddings for a list of items.
1154 * @param {string[]} items Items to embed
1155 * @returns {Promise<{embeddings: Record<string, number[]>, model: string}>} Calculated embeddings
1156 */
1157async function createKoboldCppEmbeddings(items) {
1158 const response = await fetch('/api/backends/kobold/embed', {
1159 method: 'POST',
1160 headers: getRequestHeaders(),
1161 body: JSON.stringify({
1162 items: items,
1163 server: textgenerationwebui_settings.server_urls[textgen_types.KOBOLDCPP],
1164 }),
1165 });
1166
1167 if (!response.ok) {
1168 throw new Error('Failed to get KoboldCpp embeddings');
1169 }
1170
1171 const data = await response.json();
1172 if (!Array.isArray(data.embeddings) || !data.model || data.embeddings.length !== items.length) {
1173 throw new Error('Invalid response from KoboldCpp embeddings');
1174 }
1175
1176 const embeddings = /** @type {Record<string, number[]>} */ ({});
1177 for (let i = 0; i < data.embeddings.length; i++) {
1178 if (!Array.isArray(data.embeddings[i]) || data.embeddings[i].length === 0) {
1179 throw new Error('KoboldCpp returned an empty embedding. Reduce the chunk size and/or size threshold and try again.');
1180 }
1181
1182 embeddings[items[i]] = data.embeddings[i];
1183 }
1184
1185 return {
1186 embeddings: embeddings,
1187 model: data.model,
1188 };
1189}
1190
1141async function onPurgeClick() {1191async function onPurgeClick() {
1142 const chatId = getCurrentChatId();1192 const chatId = getCurrentChatId();
1143 if (!chatId) {1193 if (!chatId) {
public/scripts/extensions/vectors/settings.html+9 -0
@@ -13,6 +13,7 @@
13 <option value="cohere">Cohere</option>13 <option value="cohere">Cohere</option>
14 <option value="extras">Extras (deprecated)</option>14 <option value="extras">Extras (deprecated)</option>
15 <option value="palm">Google AI Studio</option>15 <option value="palm">Google AI Studio</option>
16 <option value="koboldcpp">KoboldCpp</option>
16 <option value="llamacpp">llama.cpp</option>17 <option value="llamacpp">llama.cpp</option>
17 <option value="transformers" data-i18n="Local (Transformers)">Local (Transformers)</option>18 <option value="transformers" data-i18n="Local (Transformers)">Local (Transformers)</option>
18 <option value="mistral">MistralAI</option>19 <option value="mistral">MistralAI</option>
@@ -55,6 +56,14 @@
55 Hint: Set the URL in the API connection settings.56 Hint: Set the URL in the API connection settings.
56 </i>57 </i>
57 </div>58 </div>
59 <div class="flex-container flexFlowColumn" id="koboldcpp_vectorsModel">
60 <span>
61 Set the KoboldCpp URL in the Text Completion API connection settings.
62 </span>
63 <span>
64 Must use version 1.87 or higher and have an embedding model loaded.
65 </span>
66 </div>
58 <div class="flex-container flexFlowColumn" id="llamacpp_vectorsModel">67 <div class="flex-container flexFlowColumn" id="llamacpp_vectorsModel">
59 <span data-i18n="The server MUST be started with the --embedding flag to use this feature!">68 <span data-i18n="The server MUST be started with the --embedding flag to use this feature!">
60 The server MUST be started with the <code>--embedding</code> flag to use this feature!69 The server MUST be started with the <code>--embedding</code> flag to use this feature!
src/endpoints/backends/kobold.js+42 -0
@@ -237,3 +237,45 @@ router.post('/transcribe-audio', async function (request, response) {
237 response.status(500).send('Internal server error');237 response.status(500).send('Internal server error');
238 }238 }
239});239});
240
241router.post('/embed', async function (request, response) {
242 try {
243 const { server, items } = request.body;
244
245 if (!server) {
246 console.warn('KoboldCpp URL is not set');
247 return response.sendStatus(400);
248 }
249
250 const headers = {};
251 setAdditionalHeadersByType(headers, TEXTGEN_TYPES.KOBOLDCPP, server, request.user.directories);
252
253 const embeddingsUrl = new URL(server);
254 embeddingsUrl.pathname = '/api/extra/embeddings';
255
256 const embeddingsResult = await fetch(embeddingsUrl, {
257 method: 'POST',
258 headers: {
259 ...headers,
260 },
261 body: JSON.stringify({
262 input: items,
263 }),
264 });
265
266 /** @type {any} */
267 const data = await embeddingsResult.json();
268
269 if (!Array.isArray(data?.data)) {
270 console.warn('KoboldCpp API response was not an array');
271 return response.sendStatus(500);
272 }
273
274 const model = data.model || 'unknown';
275 const embeddings = data.data.map(x => Array.isArray(x) ? x[0] : x).sort((a, b) => a.index - b.index).map(x => x.embedding);
276 return response.json({ model, embeddings });
277 } catch (error) {
278 console.error('KoboldCpp embedding failed', error);
279 response.status(500).send('Internal server error');
280 }
281});
src/endpoints/vectors.js+11 -0
@@ -31,6 +31,7 @@ const SOURCES = [
31 'llamacpp',31 'llamacpp',
32 'vllm',32 'vllm',
33 'webllm',33 'webllm',
34 'koboldcpp',
34];35];
3536
36/**37/**
@@ -66,6 +67,8 @@ async function getVector(source, sourceSettings, text, isQuery, directories) {
66 return getOllamaVector(text, sourceSettings.apiUrl, sourceSettings.model, sourceSettings.keep, directories);67 return getOllamaVector(text, sourceSettings.apiUrl, sourceSettings.model, sourceSettings.keep, directories);
67 case 'webllm':68 case 'webllm':
68 return sourceSettings.embeddings[text];69 return sourceSettings.embeddings[text];
70 case 'koboldcpp':
71 return sourceSettings.embeddings[text];
69 }72 }
7073
71 throw new Error(`Unknown vector source ${source}`);74 throw new Error(`Unknown vector source ${source}`);
@@ -119,6 +122,9 @@ async function getBatchVector(source, sourceSettings, texts, isQuery, directorie
119 case 'webllm':122 case 'webllm':
120 results.push(...texts.map(x => sourceSettings.embeddings[x]));123 results.push(...texts.map(x => sourceSettings.embeddings[x]));
121 break;124 break;
125 case 'koboldcpp':
126 results.push(...texts.map(x => sourceSettings.embeddings[x]));
127 break;
122 default:128 default:
123 throw new Error(`Unknown vector source ${source}`);129 throw new Error(`Unknown vector source ${source}`);
124 }130 }
@@ -189,6 +195,11 @@ function getSourceSettings(source, request) {
189 model: String(request.body.model),195 model: String(request.body.model),
190 embeddings: request.body.embeddings ?? {},196 embeddings: request.body.embeddings ?? {},
191 };197 };
198 case 'koboldcpp':
199 return {
200 model: String(request.body.model),
201 embeddings: request.body.embeddings ?? {},
202 };
192 default:203 default:
193 return {};204 return {};
194 }205 }