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 | * @returns {Promise<boolean>} True if successful, false if not | 565 | * @returns {Promise<boolean>} True if successful, false if not |
| 566 | */ | 566 | */ |
| 567 | async function vectorizeFile(fileText, fileName, collectionId, chunkSize, overlapPercent) { | 567 | async 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 | |||
| 574 | 576 | ||
| 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 overlapChunks | 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 | 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() { | |||
| 872 | 881 | ||
| 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 | } |
| 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 | async function onPurgeClick() { | 1191 | async function onPurgeClick() { |
| 1142 | const chatId = getCurrentChatId(); | 1192 | const chatId = getCurrentChatId(); |
| 1143 | if (!chatId) { | 1193 | if (!chatId) { |
| @@ -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! |
| @@ -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 | |||
| 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 | 'llamacpp', | 31 | 'llamacpp', |
| 32 | 'vllm', | 32 | 'vllm', |
| 33 | 'webllm', | 33 | 'webllm', |
| 34 | 'koboldcpp', | ||
| 34 | ]; | 35 | ]; |
| 35 | 36 | ||
| 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 | } |
| 70 | 73 | ||
| 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 | } |