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
Signed| @@ -565,6 +565,8 @@ async function retrieveFileChunks(queryText, collectionId) { | ||
| 565 | 565 | * @returns {Promise<boolean>} True if successful, false if not |
| 566 | 566 | */ |
| 567 | 567 | async function vectorizeFile(fileText, fileName, collectionId, chunkSize, overlapPercent) { |
| 568 | + let toast = jQuery(); | |
| 569 | + | |
| 568 | 570 | try { |
| 569 | 571 | if (settings.translate_files && typeof globalThis.translate === 'function') { |
| 570 | 572 | console.log(`Vectors: Translating file ${fileName} to English...`); |
| @@ -574,7 +576,7 @@ async function vectorizeFile(fileText, fileName, collectionId, chunkSize, overla | ||
| 574 | 576 | |
| 575 | 577 | const batchSize = getBatchSize(); |
| 576 | 578 | const toastBody = $('<span>').text('This may take a while. Please wait...'); |
| 577 | 579 | const toast = toastr.info(toastBody, `Ingesting file ${escapeHtml(fileName)}`, { closeButton: false, escapeHtml: false, timeOut: 0, extendedTimeOut: 0 }); |
| 578 | 580 | const overlapSize = Math.round(chunkSize * overlapPercent / 100); |
| 579 | 581 | const delimiters = getChunkDelimiters(); |
| 580 | 582 | // 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 | 598 | console.log(`Vectors: Inserted ${chunks.length} vector items for file ${fileName} into ${collectionId}`); |
| 597 | 599 | return true; |
| 598 | 600 | } catch (error) { |
| 601 | + toastr.clear(toast); | |
| 599 | 602 | toastr.error(String(error), 'Failed to vectorize file', { preventDuplicates: true }); |
| 600 | 603 | console.error('Vectors: Failed to vectorize file', error); |
| 601 | 604 | return false; |
| @@ -803,6 +806,12 @@ async function getAdditionalArgs(items) { | ||
| 803 | 806 | case 'webllm': |
| 804 | 807 | args.embeddings = await createWebLlmEmbeddings(items); |
| 805 | 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 | 816 | return args; |
| 808 | 817 | } |
| @@ -872,6 +881,7 @@ function throwIfSourceInvalid() { | ||
| 872 | 881 | |
| 873 | 882 | if (settings.source === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA] || |
| 874 | 883 | settings.source === 'vllm' && !textgenerationwebui_settings.server_urls[textgen_types.VLLM] || |
| 884 | + settings.source === 'koboldcpp' && !textgenerationwebui_settings.server_urls[textgen_types.KOBOLDCPP] || | |
| 875 | 885 | settings.source === 'llamacpp' && !textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) { |
| 876 | 886 | throw new Error('Vectors: API URL missing', { cause: 'api_url_missing' }); |
| 877 | 887 | } |
| @@ -1071,6 +1081,7 @@ function toggleSettings() { | ||
| 1071 | 1081 | $('#vllm_vectorsModel').toggle(settings.source === 'vllm'); |
| 1072 | 1082 | $('#nomicai_apiKey').toggle(settings.source === 'nomicai'); |
| 1073 | 1083 | $('#webllm_vectorsModel').toggle(settings.source === 'webllm'); |
| 1084 | + $('#koboldcpp_vectorsModel').toggle(settings.source === 'koboldcpp'); | |
| 1074 | 1085 | if (settings.source === 'webllm') { |
| 1075 | 1086 | loadWebLlmModels(); |
| 1076 | 1087 | } |
| @@ -1138,6 +1149,45 @@ async function createWebLlmEmbeddings(items) { | ||
| 1138 | 1149 | }); |
| 1139 | 1150 | } |
| 1140 | 1151 | |
| 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 | + */ | |
| 1157 | +async 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 | + | |
| 1141 | 1191 | async function onPurgeClick() { |
| 1142 | 1192 | const chatId = getCurrentChatId(); |
| 1143 | 1193 | if (!chatId) { |
| @@ -13,6 +13,7 @@ | ||
| 13 | 13 | <option value="cohere">Cohere</option> |
| 14 | 14 | <option value="extras">Extras (deprecated)</option> |
| 15 | 15 | <option value="palm">Google AI Studio</option> |
| 16 | + <option value="koboldcpp">KoboldCpp</option> | |
| 16 | 17 | <option value="llamacpp">llama.cpp</option> |
| 17 | 18 | <option value="transformers" data-i18n="Local (Transformers)">Local (Transformers)</option> |
| 18 | 19 | <option value="mistral">MistralAI</option> |
| @@ -55,6 +56,14 @@ | ||
| 55 | 56 | Hint: Set the URL in the API connection settings. |
| 56 | 57 | </i> |
| 57 | 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 | 67 | <div class="flex-container flexFlowColumn" id="llamacpp_vectorsModel"> |
| 59 | 68 | <span data-i18n="The server MUST be started with the --embedding flag to use this feature!"> |
| 60 | 69 | The server MUST be started with the <code>--embedding</code> flag to use this feature! |
| @@ -237,3 +237,45 @@ router.post('/transcribe-audio', async function (request, response) { | ||
| 237 | 237 | response.status(500).send('Internal server error'); |
| 238 | 238 | } |
| 239 | 239 | }); |
| 240 | + | |
| 241 | +router.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 | +}); | |
| @@ -31,6 +31,7 @@ const SOURCES = [ | ||
| 31 | 31 | 'llamacpp', |
| 32 | 32 | 'vllm', |
| 33 | 33 | 'webllm', |
| 34 | + 'koboldcpp', | |
| 34 | 35 | ]; |
| 35 | 36 | |
| 36 | 37 | /** |
| @@ -66,6 +67,8 @@ async function getVector(source, sourceSettings, text, isQuery, directories) { | ||
| 66 | 67 | return getOllamaVector(text, sourceSettings.apiUrl, sourceSettings.model, sourceSettings.keep, directories); |
| 67 | 68 | case 'webllm': |
| 68 | 69 | return sourceSettings.embeddings[text]; |
| 70 | + case 'koboldcpp': | |
| 71 | + return sourceSettings.embeddings[text]; | |
| 69 | 72 | } |
| 70 | 73 | |
| 71 | 74 | throw new Error(`Unknown vector source ${source}`); |
| @@ -119,6 +122,9 @@ async function getBatchVector(source, sourceSettings, texts, isQuery, directorie | ||
| 119 | 122 | case 'webllm': |
| 120 | 123 | results.push(...texts.map(x => sourceSettings.embeddings[x])); |
| 121 | 124 | break; |
| 125 | + case 'koboldcpp': | |
| 126 | + results.push(...texts.map(x => sourceSettings.embeddings[x])); | |
| 127 | + break; | |
| 122 | 128 | default: |
| 123 | 129 | throw new Error(`Unknown vector source ${source}`); |
| 124 | 130 | } |
| @@ -189,6 +195,11 @@ function getSourceSettings(source, request) { | ||
| 189 | 195 | model: String(request.body.model), |
| 190 | 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 | 203 | default: |
| 193 | 204 | return {}; |
| 194 | 205 | } |