From ea721feea901928477e3aa38925dda110b66aa76 Mon Sep 17 00:00:00 2001 From: Anuraag Jain Date: Sat, 6 Jan 2024 12:10:41 +0200 Subject: [PATCH 001/398] feat: parallel model downloads --- package-lock.json | 11 +++ package.json | 1 + src/lib/components/chat/SettingsModal.svelte | 80 ++++++++++++++++---- 3 files changed, 79 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index af8790a041..9e32e95b4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "dependencies": { "@sveltejs/adapter-node": "^1.3.1", + "async": "^3.2.5", "file-saver": "^2.0.5", "highlight.js": "^11.9.0", "idb": "^7.1.1", @@ -1208,6 +1209,11 @@ "node": ">=8" } }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, "node_modules/autoprefixer": { "version": "10.4.16", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", @@ -4645,6 +4651,11 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, "autoprefixer": { "version": "10.4.16", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", diff --git a/package.json b/package.json index 9c3b6ddfdf..92e8fdc88a 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "type": "module", "dependencies": { "@sveltejs/adapter-node": "^1.3.1", + "async": "^3.2.5", "file-saver": "^2.0.5", "highlight.js": "^11.9.0", "idb": "^7.1.1", diff --git a/src/lib/components/chat/SettingsModal.svelte b/src/lib/components/chat/SettingsModal.svelte index a220bd4ebc..d98a8f39a4 100644 --- a/src/lib/components/chat/SettingsModal.svelte +++ b/src/lib/components/chat/SettingsModal.svelte @@ -6,6 +6,7 @@ import { onMount } from 'svelte'; import { config, models, settings, user, chats } from '$lib/stores'; import { splitStream, getGravatarURL } from '$lib/utils'; + import queue from 'async/queue'; import { getOllamaVersion } from '$lib/apis/ollama'; import { createNewChat, deleteAllChats, getAllChats, getChatList } from '$lib/apis/chats'; @@ -38,6 +39,8 @@ let theme = 'dark'; let notificationEnabled = false; let system = ''; + const modelDownloadQueue = queue((task:{modelName: string}, cb) => pullModelHandlerProcessor({modelName: task.modelName, callback: cb}), 3); + let modelDownloadStatus: Record = {}; // Advanced let requestFormat = ''; @@ -224,8 +227,9 @@ authEnabled = !authEnabled; }; - const pullModelHandler = async () => { - modelTransferring = true; + const pullModelHandlerProcessor = async (opts:{modelName:string, callback: Function}) => { + console.log('Pull model name', opts.modelName); + const res = await fetch(`${API_BASE_URL}/pull`, { method: 'POST', headers: { @@ -234,7 +238,7 @@ ...($user && { Authorization: `Bearer ${localStorage.token}` }) }, body: JSON.stringify({ - name: modelTag + name: opts.modelName }) }); @@ -265,11 +269,9 @@ } if (data.status) { if (!data.digest) { - toast.success(data.status); - if (data.status === 'success') { const notification = new Notification(`Ollama`, { - body: `Model '${modelTag}' has been successfully downloaded.`, + body: `Model '${opts.modelName}' has been successfully downloaded.`, icon: '/favicon.png' }); } @@ -280,21 +282,48 @@ } else { pullProgress = 100; } + modelDownloadStatus[opts.modelName] = {pullProgress}; } } } } } catch (error) { - console.log(error); - toast.error(error); + console.error(error); + opts.callback({success:false, error, modelName: opts.modelName}); } } + opts.callback({success: true, modelName: opts.modelName}); + }; + + const pullModelHandler = async() => { + if(modelDownloadStatus[modelTag]){ + toast.error("Model already in queue for downloading."); + return; + } + if(Object.keys(modelDownloadStatus).length === 3){ + toast.error('Maximum of 3 models can be downloading simultaneously. Please try again later'); + return; + } + modelTransferring = true; + + modelDownloadQueue.push({modelName: modelTag},async (data:{modelName: string; success: boolean; error?: Error}) => { + const {modelName} = data; + // Remove the downloaded model + delete modelDownloadStatus[modelName]; + + if(!data.success){ + toast.error(`There was some issue in downloading the model ${modelName}`); + return; + } + + toast.success(`Model ${modelName} was successfully downloaded`); + models.set(await getModels()); + }); modelTag = ''; - modelTransferring = false; + modelTransferring = false; + } - models.set(await getModels()); - }; const calculateSHA256 = async (file) => { console.log(file); @@ -1248,7 +1277,7 @@ > - {#if pullProgress !== null} + + {#if Object.keys(modelDownloadStatus).length > 0} + + + + + + + + + {#each Object.entries(modelDownloadStatus) as [modelName, payload]} + + + + + {/each} + +
Model Name Download progress
{modelName}
+ { payload.pullProgress ?? 0}% +
+ {/if}
From fd0dcec61d90ceee6f2b859cbacd0d23058cdcd8 Mon Sep 17 00:00:00 2001 From: Anuraag Jain Date: Sat, 6 Jan 2024 13:15:21 +0200 Subject: [PATCH 002/398] fix: progress bar colors for light theme --- src/lib/components/chat/SettingsModal.svelte | 34 ++++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/lib/components/chat/SettingsModal.svelte b/src/lib/components/chat/SettingsModal.svelte index d98a8f39a4..f2cb4f171d 100644 --- a/src/lib/components/chat/SettingsModal.svelte +++ b/src/lib/components/chat/SettingsModal.svelte @@ -277,12 +277,13 @@ } } else { digest = data.digest; + let downloadProgress = 0; if (data.completed) { - pullProgress = Math.round((data.completed / data.total) * 1000) / 10; + downloadProgress = Math.round((data.completed / data.total) * 1000) / 10; } else { - pullProgress = 100; + downloadProgress = 100; } - modelDownloadStatus[opts.modelName] = {pullProgress}; + modelDownloadStatus[opts.modelName] = {pullProgress: downloadProgress, digest: data.digest}; } } } @@ -1277,22 +1278,22 @@ >
- + + --> + {#if Object.keys(modelDownloadStatus).length > 0} @@ -1308,12 +1309,17 @@ {#each Object.entries(modelDownloadStatus) as [modelName, payload]} - + +
+ {payload.digest} +
+ {/each} From fd42422d6c981f7a5205f639a80b5dc6b2310c2d Mon Sep 17 00:00:00 2001 From: Anuraag Jain Date: Sat, 6 Jan 2024 15:01:47 +0200 Subject: [PATCH 003/398] refac: pullModel api --- src/lib/apis/ollama/index.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/lib/apis/ollama/index.ts b/src/lib/apis/ollama/index.ts index 10eddc1c19..5b0f987053 100644 --- a/src/lib/apis/ollama/index.ts +++ b/src/lib/apis/ollama/index.ts @@ -249,8 +249,7 @@ export const deleteModel = async (token: string, tagName: string) => { }; export const pullModel = async (token: string, tagName: string) => { - let error = null; - +try { const res = await fetch(`${OLLAMA_API_BASE_URL}/pull`, { method: 'POST', headers: { @@ -260,14 +259,9 @@ export const pullModel = async (token: string, tagName: string) => { body: JSON.stringify({ name: tagName }) - }).catch((err) => { - error = err; - return null; - }); - - if (error) { - throw error; - } - + }) return res; +} catch (error) { + throw error; +} }; From 8f570bc2ee83a87ad3654cc5e5ae4094e5fa904f Mon Sep 17 00:00:00 2001 From: Anuraag Jain Date: Sat, 6 Jan 2024 15:06:57 +0200 Subject: [PATCH 004/398] refac: code cleanup --- src/lib/components/chat/SettingsModal.svelte | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/lib/components/chat/SettingsModal.svelte b/src/lib/components/chat/SettingsModal.svelte index 3fe15bf147..4ef2f0165e 100644 --- a/src/lib/components/chat/SettingsModal.svelte +++ b/src/lib/components/chat/SettingsModal.svelte @@ -50,7 +50,8 @@ let theme = 'dark'; let notificationEnabled = false; let system = ''; - const modelDownloadQueue = queue((task:{modelName: string}, cb) => pullModelHandlerProcessor({modelName: task.modelName, callback: cb}), 3); + const MAX_PARALLEL_DOWNLOADS = 3; + const modelDownloadQueue = queue((task:{modelName: string}, cb) => pullModelHandlerProcessor({modelName: task.modelName, callback: cb}), MAX_PARALLEL_DOWNLOADS); let modelDownloadStatus: Record = {}; // Advanced @@ -1187,23 +1188,6 @@ target="_blank">click here. - - - - {#if Object.keys(modelDownloadStatus).length > 0}
{modelName}
+
{ payload.pullProgress ?? 0}% -
From 3853261b40f04648379654f693cffbce82363347 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 6 Jan 2024 13:02:09 -0800 Subject: [PATCH 005/398] refac --- src/lib/apis/ollama/index.ts | 33 +++- src/lib/components/chat/SettingsModal.svelte | 180 ++++++++----------- src/lib/utils/index.ts | 35 ++++ 3 files changed, 134 insertions(+), 114 deletions(-) diff --git a/src/lib/apis/ollama/index.ts b/src/lib/apis/ollama/index.ts index 5b0f987053..9d3ac0a849 100644 --- a/src/lib/apis/ollama/index.ts +++ b/src/lib/apis/ollama/index.ts @@ -249,7 +249,8 @@ export const deleteModel = async (token: string, tagName: string) => { }; export const pullModel = async (token: string, tagName: string) => { -try { + let error = null; + const res = await fetch(`${OLLAMA_API_BASE_URL}/pull`, { method: 'POST', headers: { @@ -259,9 +260,31 @@ try { body: JSON.stringify({ name: tagName }) - }) + }).catch((err) => { + console.log(err); + error = err; + + if ('detail' in err) { + error = err.detail; + } + + return null; + }); + if (error) { + throw error; + } return res; -} catch (error) { - throw error; -} }; + +// export const pullModel = async (token: string, tagName: string) => { +// return await fetch(`${OLLAMA_API_BASE_URL}/pull`, { +// method: 'POST', +// headers: { +// 'Content-Type': 'text/event-stream', +// Authorization: `Bearer ${token}` +// }, +// body: JSON.stringify({ +// name: tagName +// }) +// }); +// }; diff --git a/src/lib/components/chat/SettingsModal.svelte b/src/lib/components/chat/SettingsModal.svelte index 4ef2f0165e..e0f04d8272 100644 --- a/src/lib/components/chat/SettingsModal.svelte +++ b/src/lib/components/chat/SettingsModal.svelte @@ -51,7 +51,11 @@ let notificationEnabled = false; let system = ''; const MAX_PARALLEL_DOWNLOADS = 3; - const modelDownloadQueue = queue((task:{modelName: string}, cb) => pullModelHandlerProcessor({modelName: task.modelName, callback: cb}), MAX_PARALLEL_DOWNLOADS); + const modelDownloadQueue = queue( + (task: { modelName: string }, cb) => + pullModelHandlerProcessor({ modelName: task.modelName, callback: cb }), + MAX_PARALLEL_DOWNLOADS + ); let modelDownloadStatus: Record = {}; // Advanced @@ -250,11 +254,13 @@ saveSettings({ saveChatHistory: saveChatHistory }); }; - const pullModelHandlerProcessor = async (opts:{modelName:string, callback: Function}) => { + const pullModelHandlerProcessor = async (opts: { modelName: string; callback: Function }) => { + const res = await pullModel(localStorage.token, opts.modelName).catch((error) => { + opts.callback({ success: false, error, modelName: opts.modelName }); + return null; + }); - try { - const res = await pullModel(localStorage.token, opts.modelName); - + if (res) { const reader = res.body .pipeThrough(new TextDecoderStream()) .pipeThrough(splitStream('\n')) @@ -270,102 +276,70 @@ for (const line of lines) { if (line !== '') { let data = JSON.parse(line); - if (data.error) { - throw data.error; - } - if (data.detail) { - throw data.detail; - } - if (data.status) { - if (data.digest) { - let downloadProgress = 0; - if (data.completed) { - downloadProgress = Math.round((data.completed / data.total) * 1000) / 10; - } else { - downloadProgress = 100; - } - modelDownloadStatus[opts.modelName] = {pullProgress: downloadProgress, digest: data.digest}; + if (data.error) { + throw data.error; + } + if (data.detail) { + throw data.detail; + } + if (data.status) { + if (data.digest) { + let downloadProgress = 0; + if (data.completed) { + downloadProgress = Math.round((data.completed / data.total) * 1000) / 10; + } else { + downloadProgress = 100; + } + modelDownloadStatus[opts.modelName] = { + pullProgress: downloadProgress, + digest: data.digest + }; + } } - } } } } catch (error) { - console.log('Failed to read from data stream', error); - throw error; + console.log(error); + opts.callback({ success: false, error, modelName: opts.modelName }); } } - opts.callback({success: true, modelName: opts.modelName}); - } catch (error) { - console.error(error); - opts.callback({success:false, error, modelName: opts.modelName}); + opts.callback({ success: true, modelName: opts.modelName }); } + }; - - }; - - const pullModelHandler = async() => { - if(modelDownloadStatus[modelTag]){ - toast.error("Model already in queue for downloading."); + const pullModelHandler = async () => { + if (modelDownloadStatus[modelTag]) { + toast.error('Model already in queue for downloading.'); return; } - if(Object.keys(modelDownloadStatus).length === 3){ + if (Object.keys(modelDownloadStatus).length === 3) { toast.error('Maximum of 3 models can be downloading simultaneously. Please try again later'); return; } + modelTransferring = true; - modelDownloadQueue.push({modelName: modelTag},async (data:{modelName: string; success: boolean; error?: Error}) => { - const {modelName} = data; - // Remove the downloaded model - delete modelDownloadStatus[modelName]; + modelDownloadQueue.push( + { modelName: modelTag }, + async (data: { modelName: string; success: boolean; error?: Error }) => { + const { modelName } = data; + // Remove the downloaded model + delete modelDownloadStatus[modelName]; - if(!data.success){ - toast.error(`There was some issue in downloading the model ${modelName}`); - return; + console.log(data); + + if (!data.success) { + toast.error(data.error); + return; + } + + toast.success(`Model ${modelName} was successfully downloaded`); + models.set(await getModels()); } - - toast.success(`Model ${modelName} was successfully downloaded`); - models.set(await getModels()); - }); + ); modelTag = ''; - modelTransferring = false; - } - - - const calculateSHA256 = async (file) => { - console.log(file); - // Create a FileReader to read the file asynchronously - const reader = new FileReader(); - - // Define a promise to handle the file reading - const readFile = new Promise((resolve, reject) => { - reader.onload = () => resolve(reader.result); - reader.onerror = reject; - }); - - // Read the file as an ArrayBuffer - reader.readAsArrayBuffer(file); - - try { - // Wait for the FileReader to finish reading the file - const buffer = await readFile; - - // Convert the ArrayBuffer to a Uint8Array - const uint8Array = new Uint8Array(buffer); - - // Calculate the SHA-256 hash using Web Crypto API - const hashBuffer = await crypto.subtle.digest('SHA-256', uint8Array); - - // Convert the hash to a hexadecimal string - const hashArray = Array.from(new Uint8Array(hashBuffer)); - const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join(''); - - return `sha256:${hashHex}`; - } catch (error) { - console.error('Error calculating SHA-256 hash:', error); - throw error; - } + modelTransferring = false; }; const uploadModelHandler = async () => { @@ -1190,35 +1164,23 @@ {#if Object.keys(modelDownloadStatus).length > 0} -
- - - - - - - - {#each Object.entries(modelDownloadStatus) as [modelName, payload]} - - - - - {/each} - -
Model Name Download progress
{modelName} + {#each Object.entries(modelDownloadStatus) as [modelName, payload]} +
+
{modelName}
+
- { payload.pullProgress ?? 0}% + class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full" + style="width: {Math.max(15, payload.pullProgress ?? 0)}%" + > + {payload.pullProgress ?? 0}% +
+
+ {payload.digest} +
-
- {payload.digest} -
-
- {/if} + + {/each} + {/if}
diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 2029604e04..d9f6fd7d1e 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -127,3 +127,38 @@ export const findWordIndices = (text) => { return matches; }; + +export const calculateSHA256 = async (file) => { + console.log(file); + // Create a FileReader to read the file asynchronously + const reader = new FileReader(); + + // Define a promise to handle the file reading + const readFile = new Promise((resolve, reject) => { + reader.onload = () => resolve(reader.result); + reader.onerror = reject; + }); + + // Read the file as an ArrayBuffer + reader.readAsArrayBuffer(file); + + try { + // Wait for the FileReader to finish reading the file + const buffer = await readFile; + + // Convert the ArrayBuffer to a Uint8Array + const uint8Array = new Uint8Array(buffer); + + // Calculate the SHA-256 hash using Web Crypto API + const hashBuffer = await crypto.subtle.digest('SHA-256', uint8Array); + + // Convert the hash to a hexadecimal string + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join(''); + + return `sha256:${hashHex}`; + } catch (error) { + console.error('Error calculating SHA-256 hash:', error); + throw error; + } +}; From 84f0cb41bb8cac00ac42106a350d7bd56842bd6d Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 6 Jan 2024 13:23:08 -0800 Subject: [PATCH 006/398] fix: restore status toast --- src/lib/components/chat/SettingsModal.svelte | 50 +++++++++++--------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/lib/components/chat/SettingsModal.svelte b/src/lib/components/chat/SettingsModal.svelte index e0f04d8272..108e631cde 100644 --- a/src/lib/components/chat/SettingsModal.svelte +++ b/src/lib/components/chat/SettingsModal.svelte @@ -295,11 +295,16 @@ digest: data.digest }; } + } else { + toast.success(data.status); } } } } catch (error) { console.log(error); + if (typeof error !== 'string') { + error = error.message; + } opts.callback({ success: false, error, modelName: opts.modelName }); } } @@ -330,11 +335,10 @@ if (!data.success) { toast.error(data.error); - return; + } else { + toast.success(`Model ${modelName} was successfully downloaded`); + models.set(await getModels()); } - - toast.success(`Model ${modelName} was successfully downloaded`); - models.set(await getModels()); } ); @@ -1155,32 +1159,34 @@
-
+
To access the available model names for downloading, click here.
-
- {#if Object.keys(modelDownloadStatus).length > 0} - {#each Object.entries(modelDownloadStatus) as [modelName, payload]} -
-
{modelName}
-
-
- {payload.pullProgress ?? 0}% -
-
- {payload.digest} + + {#if Object.keys(modelDownloadStatus).length > 0} + {#each Object.entries(modelDownloadStatus) as [modelName, payload]} +
+
{modelName}
+
+
+ {payload.pullProgress ?? 0}% +
+
+ {payload.digest} +
-
- {/each} - {/if} + {/each} + {/if} +
+
From cf23c231346b4543092f37367c48e8e5b9fb0141 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 6 Jan 2024 13:24:11 -0800 Subject: [PATCH 007/398] fix --- src/lib/components/chat/SettingsModal.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/components/chat/SettingsModal.svelte b/src/lib/components/chat/SettingsModal.svelte index 108e631cde..59d406c1a1 100644 --- a/src/lib/components/chat/SettingsModal.svelte +++ b/src/lib/components/chat/SettingsModal.svelte @@ -294,9 +294,9 @@ pullProgress: downloadProgress, digest: data.digest }; + } else { + toast.success(data.status); } - } else { - toast.success(data.status); } } } From e7d8d4937455e684ea4b60b9553be300dbc9f44f Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 6 Jan 2024 13:27:02 -0800 Subject: [PATCH 008/398] fix: restore download notification --- src/lib/components/chat/SettingsModal.svelte | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib/components/chat/SettingsModal.svelte b/src/lib/components/chat/SettingsModal.svelte index 59d406c1a1..d82b640b32 100644 --- a/src/lib/components/chat/SettingsModal.svelte +++ b/src/lib/components/chat/SettingsModal.svelte @@ -337,6 +337,12 @@ toast.error(data.error); } else { toast.success(`Model ${modelName} was successfully downloaded`); + + const notification = new Notification(`Ollama`, { + body: `Model '${modelName}' has been successfully downloaded.`, + icon: '/favicon.png' + }); + models.set(await getModels()); } } From 560dfd80dd9f48d241b2a1a31a421999315b5365 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 6 Jan 2024 13:30:43 -0800 Subject: [PATCH 009/398] refac: explicit var name for ambiguous term payload --- src/lib/components/chat/SettingsModal.svelte | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/lib/components/chat/SettingsModal.svelte b/src/lib/components/chat/SettingsModal.svelte index d82b640b32..047125ffac 100644 --- a/src/lib/components/chat/SettingsModal.svelte +++ b/src/lib/components/chat/SettingsModal.svelte @@ -1174,18 +1174,21 @@
{#if Object.keys(modelDownloadStatus).length > 0} - {#each Object.entries(modelDownloadStatus) as [modelName, payload]} + {#each Object.keys(modelDownloadStatus) as model}
-
{modelName}
+
{modelDownloadStatus[model].modelName}
- {payload.pullProgress ?? 0}% + {modelDownloadStatus[model].pullProgress ?? 0}%
- {payload.digest} + {modelDownloadStatus[model].digest}
From 1482119af7b799fa0e105bb0327a2737b07413fb Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 6 Jan 2024 13:31:35 -0800 Subject: [PATCH 010/398] fix --- src/lib/components/chat/SettingsModal.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/chat/SettingsModal.svelte b/src/lib/components/chat/SettingsModal.svelte index 047125ffac..c5a430e8b5 100644 --- a/src/lib/components/chat/SettingsModal.svelte +++ b/src/lib/components/chat/SettingsModal.svelte @@ -1176,7 +1176,7 @@ {#if Object.keys(modelDownloadStatus).length > 0} {#each Object.keys(modelDownloadStatus) as model}
-
{modelDownloadStatus[model].modelName}
+
{model}
Date: Sat, 6 Jan 2024 13:35:25 -0800 Subject: [PATCH 011/398] refac --- src/lib/components/chat/SettingsModal.svelte | 82 ++++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/src/lib/components/chat/SettingsModal.svelte b/src/lib/components/chat/SettingsModal.svelte index c5a430e8b5..7c2878274a 100644 --- a/src/lib/components/chat/SettingsModal.svelte +++ b/src/lib/components/chat/SettingsModal.svelte @@ -1,12 +1,11 @@ + +{#if filteredDocs.length > 0} +
+
+
+
#
+
+ +
+
+ {#each filteredDocs as doc, docIdx} + + {/each} +
+
+
+
+{/if} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index a43104adde..260e675ec5 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -11,6 +11,13 @@ export const WEB_UI_VERSION = 'v1.0.0-alpha-static'; export const REQUIRED_OLLAMA_VERSION = '0.1.16'; +export const SUPPORTED_FILE_TYPE = [ + 'application/pdf', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'text/plain', + 'text/csv' +]; + // Source: https://kit.svelte.dev/docs/modules#$env-static-public // This feature, akin to $env/static/private, exclusively incorporates environment variables // that are prefixed with config.kit.env.publicPrefix (usually set to PUBLIC_). diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 55c83b2534..c7d8f5e624 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -11,8 +11,23 @@ export const chatId = writable(''); export const chats = writable([]); export const models = writable([]); + export const modelfiles = writable([]); export const prompts = writable([]); +export const documents = writable([ + { + collection_name: 'collection_name', + filename: 'filename', + name: 'name', + title: 'title' + }, + { + collection_name: 'collection_name1', + filename: 'filename1', + name: 'name1', + title: 'title1' + } +]); export const settings = writable({}); export const showSettings = writable(false); diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 46bc8f0464..75cd073edc 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -128,6 +128,24 @@ export const findWordIndices = (text) => { return matches; }; +export const removeFirstHashWord = (inputString) => { + // Split the string into an array of words + const words = inputString.split(' '); + + // Find the index of the first word that starts with # + const index = words.findIndex((word) => word.startsWith('#')); + + // Remove the first word with # + if (index !== -1) { + words.splice(index, 1); + } + + // Join the remaining words back into a string + const resultString = words.join(' '); + + return resultString; +}; + export const calculateSHA256 = async (file) => { // Create a FileReader to read the file asynchronously const reader = new FileReader(); diff --git a/src/routes/(app)/documents/+page.svelte b/src/routes/(app)/documents/+page.svelte index e69de29bb2..c28390f179 100644 --- a/src/routes/(app)/documents/+page.svelte +++ b/src/routes/(app)/documents/+page.svelte @@ -0,0 +1,306 @@ + + +
+
+
+
+
My Documents
+
+ +
+
+
+ + + +
+ +
+ + +
+ +
+ {#if $documents.length === 0 || dragged} +
+ +
+ {:else} + {#each $documents.filter((p) => query === '' || p.name.includes(query)) as doc} +
+
+ +
+ + + + + + + + + +
+
+ {/each} + {/if} +
+ + {#if $documents.length != 0} +
+ +
+
+ { + console.log(importFiles); + + const reader = new FileReader(); + reader.onload = async (event) => { + const savedDocs = JSON.parse(event.target.result); + console.log(savedDocs); + + for (const doc of savedDocs) { + await createNewDoc( + localStorage.token, + doc.collection_name, + doc.filename, + doc.name, + doc.title + ).catch((error) => { + toast.error(error); + return null; + }); + } + + await documents.set(await getDocs(localStorage.token)); + }; + + reader.readAsText(importFiles[0]); + }} + /> + + + + + + +
+
+ {/if} +
+
+
From df3d95bf2a445ac0bac8d8235da9d4402f0985d8 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Mon, 8 Jan 2024 01:12:02 -0800 Subject: [PATCH 015/398] refac: message drag file input --- src/lib/components/chat/MessageInput.svelte | 24 +++++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index c81d999c27..34f4de180a 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -119,12 +119,16 @@ onMount(() => { const dropZone = document.querySelector('body'); - dropZone?.addEventListener('dragover', (e) => { + const onDragOver = (e) => { e.preventDefault(); dragged = true; - }); + }; - dropZone.addEventListener('drop', async (e) => { + const onDragLeave = () => { + dragged = false; + }; + + const onDrop = async (e) => { e.preventDefault(); console.log(e); @@ -158,11 +162,17 @@ } dragged = false; - }); + }; - dropZone?.addEventListener('dragleave', () => { - dragged = false; - }); + dropZone?.addEventListener('dragover', onDragOver); + dropZone?.addEventListener('drop', onDrop); + dropZone?.addEventListener('dragleave', onDragLeave); + + return () => { + dropZone?.removeEventListener('dragover', onDragOver); + dropZone?.removeEventListener('drop', onDrop); + dropZone?.removeEventListener('dragleave', onDragLeave); + }; }); From 54c4e0761a5b9e5d102c1c0dface786cf489fdd7 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Mon, 8 Jan 2024 01:26:15 -0800 Subject: [PATCH 016/398] feat: documents file upload --- backend/apps/rag/main.py | 12 +- src/routes/(app)/+layout.svelte | 16 +- src/routes/(app)/documents/+page.svelte | 262 ++++++++++++++---------- 3 files changed, 174 insertions(+), 116 deletions(-) diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py index 6e4f5c09cb..0d6cc732ec 100644 --- a/backend/apps/rag/main.py +++ b/backend/apps/rag/main.py @@ -119,7 +119,11 @@ def store_web(form_data: StoreWebForm, user=Depends(get_current_user)): loader = WebBaseLoader(form_data.url) data = loader.load() store_data_in_vector_db(data, form_data.collection_name) - return {"status": True, "collection_name": form_data.collection_name} + return { + "status": True, + "collection_name": form_data.collection_name, + "filename": form_data.url, + } except Exception as e: print(e) raise HTTPException( @@ -176,7 +180,11 @@ def store_doc( result = store_data_in_vector_db(data, collection_name) if result: - return {"status": True, "collection_name": collection_name} + return { + "status": True, + "collection_name": collection_name, + "filename": filename, + } else: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index c264592e83..39ae0eeac5 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -13,13 +13,22 @@ import { getOpenAIModels } from '$lib/apis/openai'; - import { user, showSettings, settings, models, modelfiles, prompts } from '$lib/stores'; + import { + user, + showSettings, + settings, + models, + modelfiles, + prompts, + documents + } from '$lib/stores'; import { REQUIRED_OLLAMA_VERSION, WEBUI_API_BASE_URL } from '$lib/constants'; import SettingsModal from '$lib/components/chat/SettingsModal.svelte'; import Sidebar from '$lib/components/layout/Sidebar.svelte'; import { checkVersion } from '$lib/utils'; import ShortcutsModal from '$lib/components/chat/ShortcutsModal.svelte'; + import { getDocs } from '$lib/apis/documents'; let ollamaVersion = ''; let loaded = false; @@ -93,11 +102,10 @@ console.log(); await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}')); + await modelfiles.set(await getModelfiles(localStorage.token)); - await prompts.set(await getPrompts(localStorage.token)); - - console.log($modelfiles); + await documents.set(await getDocs(localStorage.token)); modelfiles.subscribe(async () => { // should fetch models diff --git a/src/routes/(app)/documents/+page.svelte b/src/routes/(app)/documents/+page.svelte index c28390f179..747adec8d9 100644 --- a/src/routes/(app)/documents/+page.svelte +++ b/src/routes/(app)/documents/+page.svelte @@ -6,10 +6,13 @@ import { onMount } from 'svelte'; import { documents } from '$lib/stores'; import { createNewDoc, deleteDocByName, getDocs } from '$lib/apis/documents'; - import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte'; + import { SUPPORTED_FILE_TYPE } from '$lib/constants'; + import { uploadDocToVectorDB } from '$lib/apis/rag'; let importFiles = ''; + + let inputFiles = ''; let query = ''; let dragged = false; @@ -19,40 +22,51 @@ await documents.set(await getDocs(localStorage.token)); }; - onMount(() => { - // const dropZone = document.querySelector('body'); - const dropZone = document.getElementById('dropzone'); + const uploadDoc = async (file) => { + const res = await uploadDocToVectorDB(localStorage.token, '', file); - dropZone?.addEventListener('dragover', (e) => { - e.preventDefault(); - dragged = true; - }); + if (res) { + await createNewDoc( + localStorage.token, + res.collection_name, + res.filename, + res.filename, + res.filename + ); + await documents.set(await getDocs(localStorage.token)); + } + }; - dropZone?.addEventListener('drop', async (e) => { - e.preventDefault(); - console.log(e); + const onDragOver = (e) => { + e.preventDefault(); + dragged = true; + }; - if (e.dataTransfer?.files) { - const inputFiles = e.dataTransfer?.files; + const onDragLeave = () => { + dragged = false; + }; - if (inputFiles && inputFiles.length > 0) { - const file = inputFiles[0]; - if (SUPPORTED_FILE_TYPE.includes(file['type'])) { - console.log(file); - // uploadDoc(file); - } else { - toast.error(`Unsupported File Type '${file['type']}'.`); - } + const onDrop = async (e) => { + e.preventDefault(); + console.log(e); + + if (e.dataTransfer?.files) { + const inputFiles = e.dataTransfer?.files; + + if (inputFiles && inputFiles.length > 0) { + const file = inputFiles[0]; + if (SUPPORTED_FILE_TYPE.includes(file['type'])) { + uploadDoc(file); } else { - toast.error(`File not found.`); + toast.error(`Unsupported File Type '${file['type']}'.`); } + } else { + toast.error(`File not found.`); } - }); + } - dropZone?.addEventListener('dragleave', () => { - dragged = false; - }); - }); + dragged = false; + };
@@ -86,9 +100,11 @@
-
- {#if $documents.length === 0 || dragged} -
- + { + if (inputFiles && inputFiles.length > 0) { + const file = inputFiles[0]; + if (SUPPORTED_FILE_TYPE.includes(file['type'])) { + uploadDoc(file); + } else { + toast.error(`Unsupported File Type '${file['type']}'.`); + } + + inputFiles = null; + e.target.value = ''; + } else { + toast.error(`File not found.`); + } + }} + /> + +
+
+
+
Add Files
+ +
+ Drop any files here to add to the conversation +
- {:else} - {#each $documents.filter((p) => query === '' || p.name.includes(query)) as doc} -
- + + {#each $documents.filter((p) => query === '' || p.name.includes(query)) as doc} +
+
+
+ +
+
#{doc.name} ({doc.filename})
+
+ {doc.title} +
-
- - - - - - - + + + - -
-
- {/each} - {/if} -
+ --> + +
+
+ {/each} {#if $documents.length != 0}
From fe997abc6d4908140d3ceba78ebc251b83dce6ab Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Mon, 8 Jan 2024 01:32:55 -0800 Subject: [PATCH 017/398] feat: transform filename to name --- src/lib/utils/index.ts | 13 +++++ src/routes/(app)/documents/+page.svelte | 65 +++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 75cd073edc..129a1d12c1 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -146,6 +146,19 @@ export const removeFirstHashWord = (inputString) => { return resultString; }; +export const transformFileName = (fileName) => { + // Convert to lowercase + const lowerCaseFileName = fileName.toLowerCase(); + + // Remove special characters using regular expression + const sanitizedFileName = lowerCaseFileName.replace(/[^\w\s]/g, ''); + + // Replace spaces with dashes + const finalFileName = sanitizedFileName.replace(/\s+/g, '-'); + + return finalFileName; +}; + export const calculateSHA256 = async (file) => { // Create a FileReader to read the file asynchronously const reader = new FileReader(); diff --git a/src/routes/(app)/documents/+page.svelte b/src/routes/(app)/documents/+page.svelte index 747adec8d9..f0ad1a5418 100644 --- a/src/routes/(app)/documents/+page.svelte +++ b/src/routes/(app)/documents/+page.svelte @@ -9,6 +9,7 @@ import { SUPPORTED_FILE_TYPE } from '$lib/constants'; import { uploadDocToVectorDB } from '$lib/apis/rag'; + import { transformFileName } from '$lib/utils'; let importFiles = ''; @@ -30,7 +31,7 @@ localStorage.token, res.collection_name, res.filename, - res.filename, + transformFileName(res.filename), res.filename ); await documents.set(await getDocs(localStorage.token)); @@ -165,14 +166,70 @@
- -
Date: Mon, 8 Jan 2024 01:49:20 -0800 Subject: [PATCH 018/398] feat: documents backend integration --- backend/apps/web/models/documents.py | 5 +- backend/apps/web/routers/documents.py | 4 +- backend/constants.py | 1 + src/lib/apis/documents/index.ts | 2 +- .../components/documents/EditDocModal.svelte | 151 ++++++++++++++++++ src/routes/(app)/documents/+page.svelte | 20 ++- 6 files changed, 174 insertions(+), 9 deletions(-) create mode 100644 src/lib/components/documents/EditDocModal.svelte diff --git a/backend/apps/web/models/documents.py b/backend/apps/web/models/documents.py index ccd7619451..0196c38bbe 100644 --- a/backend/apps/web/models/documents.py +++ b/backend/apps/web/models/documents.py @@ -105,9 +105,10 @@ class DocumentsTable: ).where(Document.name == name) query.execute() - doc = Document.get(Document.name == name) + doc = Document.get(Document.name == form_data.name) return DocumentModel(**model_to_dict(doc)) - except: + except Exception as e: + print(e) return None def delete_doc_by_name(self, name: str) -> bool: diff --git a/backend/apps/web/routers/documents.py b/backend/apps/web/routers/documents.py index 42463f5147..9cc0dd5d17 100644 --- a/backend/apps/web/routers/documents.py +++ b/backend/apps/web/routers/documents.py @@ -97,8 +97,8 @@ async def update_doc_by_name( return doc else: raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NAME_TAG_TAKEN, ) diff --git a/backend/constants.py b/backend/constants.py index 0f7a46a0a0..b923ec8baf 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -18,6 +18,7 @@ class ERROR_MESSAGES(str, Enum): "Uh-oh! This username is already registered. Please choose another username." ) COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string." + NAME_TAG_TAKEN = "Uh-oh! This name tag is already registered. Please choose another name tag string." INVALID_TOKEN = ( "Your session has expired or the token is invalid. Please sign in again." ) diff --git a/src/lib/apis/documents/index.ts b/src/lib/apis/documents/index.ts index 37bdde04ce..fb208ea48e 100644 --- a/src/lib/apis/documents/index.ts +++ b/src/lib/apis/documents/index.ts @@ -111,7 +111,7 @@ type DocUpdateForm = { export const updateDocByName = async (token: string, name: string, form: DocUpdateForm) => { let error = null; - const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/name/${name}/update`, { + const res = await fetch(`${WEBUI_API_BASE_URL}/documents/name/${name}/update`, { method: 'POST', headers: { Accept: 'application/json', diff --git a/src/lib/components/documents/EditDocModal.svelte b/src/lib/components/documents/EditDocModal.svelte new file mode 100644 index 0000000000..36be379525 --- /dev/null +++ b/src/lib/components/documents/EditDocModal.svelte @@ -0,0 +1,151 @@ + + + +
+
+
Edit Doc
+ +
+
+ +
+
+
{ + submitHandler(); + }} + > +
+
+
Name Tag
+ +
+
+ # +
+ +
+ + +
+ +
+
Title
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+
+ + diff --git a/src/routes/(app)/documents/+page.svelte b/src/routes/(app)/documents/+page.svelte index f0ad1a5418..fa290999ab 100644 --- a/src/routes/(app)/documents/+page.svelte +++ b/src/routes/(app)/documents/+page.svelte @@ -11,11 +11,16 @@ import { uploadDocToVectorDB } from '$lib/apis/rag'; import { transformFileName } from '$lib/utils'; + import EditDocModal from '$lib/components/documents/EditDocModal.svelte'; + let importFiles = ''; let inputFiles = ''; let query = ''; + let showEditDocModal = false; + let selectedDoc; + let dragged = false; const deleteDoc = async (name) => { @@ -70,6 +75,10 @@ }; +{#key selectedDoc} + +{/key} +
@@ -156,7 +165,7 @@
Add Files
- Drop any files here to add to the conversation + Drop any files here to add to my documents
@@ -232,10 +241,13 @@
+
+
+
+{/if} diff --git a/src/lib/components/chat/MessageInput/Suggestions.svelte b/src/lib/components/chat/MessageInput/Suggestions.svelte index f0d4b8815d..aa42aa1a04 100644 --- a/src/lib/components/chat/MessageInput/Suggestions.svelte +++ b/src/lib/components/chat/MessageInput/Suggestions.svelte @@ -9,7 +9,7 @@ + +
+
or
+ + +
+
+ diff --git a/src/lib/components/common/Modal.svelte b/src/lib/components/common/Modal.svelte index 0fb3f8bd16..b292b910f4 100644 --- a/src/lib/components/common/Modal.svelte +++ b/src/lib/components/common/Modal.svelte @@ -8,7 +8,9 @@ let mounted = false; const sizeToWidth = (size) => { - if (size === 'sm') { + if (size === 'xs') { + return 'w-[16rem]'; + } else if (size === 'sm') { return 'w-[30rem]'; } else { return 'w-[40rem]'; diff --git a/src/lib/components/layout/Navbar.svelte b/src/lib/components/layout/Navbar.svelte index bd52bd9b50..e75dafd676 100644 --- a/src/lib/components/layout/Navbar.svelte +++ b/src/lib/components/layout/Navbar.svelte @@ -5,11 +5,14 @@ import { getChatById } from '$lib/apis/chats'; import { chatId, modelfiles } from '$lib/stores'; + import ShareChatModal from '../chat/ShareChatModal.svelte'; export let initNewChat: Function; export let title: string = 'Ollama Web UI'; export let shareEnabled: boolean = false; + let showShareChatModal = false; + const shareChat = async () => { const chat = (await getChatById(localStorage.token, $chatId)).chat; console.log('share', chat); @@ -53,16 +56,17 @@ }; + From 93ec04003a05b146161de0c99ee6f376e918e2e0 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Wed, 17 Jan 2024 21:03:53 -0800 Subject: [PATCH 077/398] fix: share modal styling --- src/lib/components/chat/ShareChatModal.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/components/chat/ShareChatModal.svelte b/src/lib/components/chat/ShareChatModal.svelte index f834c421dc..3e9e30c422 100644 --- a/src/lib/components/chat/ShareChatModal.svelte +++ b/src/lib/components/chat/ShareChatModal.svelte @@ -21,10 +21,10 @@
-
or
+
or
From 287668f84ea295ace9517960029c048b1a98d5a4 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Wed, 17 Jan 2024 22:49:58 -0800 Subject: [PATCH 079/398] feat: tag add/remove frontend --- src/lib/components/layout/Navbar.svelte | 127 +++++++++++++++++++++--- 1 file changed, 112 insertions(+), 15 deletions(-) diff --git a/src/lib/components/layout/Navbar.svelte b/src/lib/components/layout/Navbar.svelte index e75dafd676..521e7abcc2 100644 --- a/src/lib/components/layout/Navbar.svelte +++ b/src/lib/components/layout/Navbar.svelte @@ -6,6 +6,7 @@ import { getChatById } from '$lib/apis/chats'; import { chatId, modelfiles } from '$lib/stores'; import ShareChatModal from '../chat/ShareChatModal.svelte'; + import { stringify } from 'postcss'; export let initNewChat: Function; export let title: string = 'Ollama Web UI'; @@ -13,6 +14,24 @@ let showShareChatModal = false; + let tags = [ + // { + // name: 'general' + // }, + // { + // name: 'medicine' + // }, + // { + // name: 'cooking' + // }, + // { + // name: 'education' + // } + ]; + + let tagName = ''; + let showTagInput = false; + const shareChat = async () => { const chat = (await getChatById(localStorage.token, $chatId)).chat; console.log('share', chat); @@ -54,6 +73,20 @@ saveAs(blob, `chat-${chat.title}.txt`); }; + + const addTag = () => { + if (!tags.find((e) => e.name === tagName)) { + tags = [ + ...tags, + { + name: JSON.parse(JSON.stringify(tagName)) + } + ]; + } + + tagName = ''; + showTagInput = false; + }; @@ -93,23 +126,87 @@
-
-
-
Add Tags
-
- + {#each tags as tag} +
+
+ {tag.name} +
+
+ {/each} + +
+ {#if showTagInput} +
+ + + +
+ + + {/if} + +
From 077f1fa34bea06f61545feea4775264b7b6a7792 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Thu, 18 Jan 2024 00:58:45 -0800 Subject: [PATCH 080/398] feat: convo tagging backend support --- backend/apps/web/models/tags.py | 180 ++++++++++++++++++++++++++++++ backend/apps/web/routers/chats.py | 65 +++++++++++ 2 files changed, 245 insertions(+) create mode 100644 backend/apps/web/models/tags.py diff --git a/backend/apps/web/models/tags.py b/backend/apps/web/models/tags.py new file mode 100644 index 0000000000..eb33b31b1e --- /dev/null +++ b/backend/apps/web/models/tags.py @@ -0,0 +1,180 @@ +from pydantic import BaseModel +from typing import List, Union, Optional +from peewee import * +from playhouse.shortcuts import model_to_dict + +import json +import uuid +import time + +from apps.web.internal.db import DB + +#################### +# Tag DB Schema +#################### + + +class Tag(Model): + name = CharField(unique=True) + user_id = CharField() + data = TextField(null=True) + + class Meta: + database = DB + + +class ChatIdTag(Model): + tag_name = ForeignKeyField(Tag, backref="chat_id_tags") + chat_id = CharField() + user_id = CharField() + timestamp = DateField() + + class Meta: + database = DB + + +class TagModel(BaseModel): + name: str + user_id: str + data: Optional[str] = None + + +class ChatIdTagModel(BaseModel): + tag_name: str + chat_id: str + user_id: str + timestamp: int + + +#################### +# Forms +#################### + + +class ChatIdTagForm(BaseModel): + tag_name: str + chat_id: str + + +class TagChatIdsResponse(BaseModel): + chat_ids: List[str] + + +class ChatTagsResponse(BaseModel): + tags: List[str] + + +class TagTable: + def __init__(self, db): + self.db = db + db.create_tables([Tag, ChatIdTag]) + + def insert_new_tag(self, name: str, user_id: str) -> Optional[TagModel]: + tag = TagModel(**{"user_id": user_id, "name": name}) + try: + result = Tag.create(**tag.model_dump()) + if result: + return tag + else: + return None + except: + return None + + def get_tag_by_name_and_user_id( + self, name: str, user_id: str + ) -> Optional[TagModel]: + try: + tag = Tag.get(Tag.name == name, Tag.user_id == user_id) + return TagModel(**model_to_dict(tag)) + except: + return None + + def add_tag_to_chat( + self, user_id: str, form_data: ChatIdTagForm + ) -> Optional[ChatTagsResponse]: + tag = self.get_tag_by_name_and_user_id(form_data.tag_name, user_id) + if tag == None: + tag = self.insert_new_tag(form_data.tag_name, user_id) + + chatIdTag = ChatIdTagModel(**{"user_id": user_id, "tag_name": tag.name}) + try: + result = ChatIdTag.create(**chatIdTag.model_dump()) + if result: + return chatIdTag + else: + return None + except: + return None + + def get_tags_by_chat_id_and_user_id( + self, chat_id: str, user_id: str + ) -> List[TagModel]: + return [ + TagModel(**model_to_dict(tag)) + for tag in Tag.select().where( + Tag.name + in [ + ChatIdTagModel(**model_to_dict(chat_id_tag)).tag_name + for chat_id_tag in ChatIdTag.select() + .where( + (ChatIdTag.user_id == user_id) & (ChatIdTag.chat_id == chat_id) + ) + .order_by(ChatIdTag.timestamp.desc()) + ] + ) + ] + + def get_chat_ids_by_tag_name_and_user_id( + self, tag_name: str, user_id: str + ) -> Optional[ChatIdTagModel]: + return [ + ChatIdTagModel(**model_to_dict(chat_id_tag)) + for chat_id_tag in ChatIdTag.select() + .where((ChatIdTag.user_id == user_id) & (ChatIdTag.tag_name == tag_name)) + .order_by(ChatIdTag.timestamp.desc()) + ] + + def count_chat_ids_by_tag_name_and_user_id( + self, tag_name: str, user_id: str + ) -> int: + return ( + ChatIdTag.select() + .where((ChatIdTag.tag_name == tag_name) & (ChatIdTag.user_id == user_id)) + .count() + ) + + def delete_tag_by_tag_name_and_chat_id_and_user_id( + self, tag_name: str, chat_id: str, user_id: str + ) -> bool: + try: + query = ChatIdTag.delete().where( + (ChatIdTag.tag_name == tag_name) + & (ChatIdTag.chat_id == chat_id) + & (ChatIdTag.user_id == user_id) + ) + query.execute() # Remove the rows, return number of rows removed. + + tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id) + if tag_count == 0: + # Remove tag item from Tag col as well + query = Tag.delete().where( + (Tag.name == tag_name) & (Tag.user_id == user_id) + ) + query.execute() # Remove the rows, return number of rows removed. + + return True + except: + return False + + def delete_tags_by_chat_id_and_user_id(self, chat_id: str, user_id: str) -> bool: + tags = self.get_tags_by_chat_id_and_user_id(chat_id, user_id) + + for tag in tags: + self.delete_tag_by_tag_name_and_chat_id_and_user_id( + tag.tag_name, chat_id, user_id + ) + + return True + + +Tags = TagTable(DB) diff --git a/backend/apps/web/routers/chats.py b/backend/apps/web/routers/chats.py index e97e14730f..a419a41223 100644 --- a/backend/apps/web/routers/chats.py +++ b/backend/apps/web/routers/chats.py @@ -16,6 +16,14 @@ from apps.web.models.chats import ( Chats, ) + +from apps.web.models.tags import ( + TagModel, + ChatIdTagForm, + ChatTagsResponse, + Tags, +) + from utils.utils import ( bearer_scheme, ) @@ -115,6 +123,63 @@ async def delete_chat_by_id(id: str, user=Depends(get_current_user)): return result +############################ +# GetChatTagsById +############################ + + +@router.get("/{id}/tags", response_model=List[TagModel]) +async def get_chat_tags_by_id(id: str, user=Depends(get_current_user)): + tags = Tags.get_tags_by_chat_id_and_user_id(id, user.id) + + if tags: + return tags + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND + ) + + +############################ +# AddChatTagById +############################ + + +@router.post("/{id}/tags", response_model=Optional[ChatTagsResponse]) +async def add_chat_tag_by_id( + id: str, form_data: ChatIdTagForm, user=Depends(get_current_user) +): + tag = Tags.add_tag_to_chat(user.id, {"tag_name": form_data.tag_name, "chat_id": id}) + + if tag: + return tag + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND + ) + + +############################ +# DeleteChatTagById +############################ + + +@router.delete("/{id}/tags", response_model=Optional[bool]) +async def add_chat_tag_by_id( + id: str, form_data: ChatIdTagForm, user=Depends(get_current_user) +): + tag = Tags.delete_tag_by_tag_name_and_chat_id_and_user_id( + form_data.tag_name, id, user.id + ) + + if tag: + return tag + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND + ) + + ############################ # DeleteAllChats ############################ From d5ed119687cfb8a27e089dc185ba563372e94189 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Thu, 18 Jan 2024 01:04:24 -0800 Subject: [PATCH 081/398] feat: convo tagging api added --- backend/apps/web/routers/chats.py | 25 +++++- src/lib/apis/chats/index.ts | 135 ++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 4 deletions(-) diff --git a/backend/apps/web/routers/chats.py b/backend/apps/web/routers/chats.py index a419a41223..0c9aa57367 100644 --- a/backend/apps/web/routers/chats.py +++ b/backend/apps/web/routers/chats.py @@ -165,15 +165,32 @@ async def add_chat_tag_by_id( @router.delete("/{id}/tags", response_model=Optional[bool]) -async def add_chat_tag_by_id( +async def delete_chat_tag_by_id( id: str, form_data: ChatIdTagForm, user=Depends(get_current_user) ): - tag = Tags.delete_tag_by_tag_name_and_chat_id_and_user_id( + result = Tags.delete_tag_by_tag_name_and_chat_id_and_user_id( form_data.tag_name, id, user.id ) - if tag: - return tag + if result: + return result + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND + ) + + +############################ +# DeleteAllChatTagsById +############################ + + +@router.delete("/{id}/tags/all", response_model=Optional[bool]) +async def delete_all_chat_tags_by_id(id: str, user=Depends(get_current_user)): + result = Tags.delete_tags_by_chat_id_and_user_id(id, user.id) + + if result: + return result else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND diff --git a/src/lib/apis/chats/index.ts b/src/lib/apis/chats/index.ts index 0eddf5b4a7..b7f01c6e9a 100644 --- a/src/lib/apis/chats/index.ts +++ b/src/lib/apis/chats/index.ts @@ -192,6 +192,141 @@ export const deleteChatById = async (token: string, id: string) => { return res; }; +export const getTagsById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/tags`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const addTagById = async (token: string, id: string, tagName: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/tags`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + tag_name: tagName, + chat_id: id + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteTagById = async (token: string, id: string, tagName: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/tags`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + tag_name: tagName, + chat_id: id + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; +export const deleteTagsById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/tags/all`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const deleteAllChats = async (token: string) => { let error = null; From 987685dbf9223197d66c77fac82033fb7bfd2528 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Thu, 18 Jan 2024 02:10:07 -0800 Subject: [PATCH 082/398] feat: convo tagging full integration --- backend/apps/web/models/tags.py | 55 +++++--- backend/apps/web/routers/chats.py | 22 +++- src/lib/components/layout/Navbar.svelte | 168 +++++++++++------------- src/routes/(app)/+page.svelte | 28 +++- src/routes/(app)/c/[id]/+page.svelte | 31 ++++- 5 files changed, 185 insertions(+), 119 deletions(-) diff --git a/backend/apps/web/models/tags.py b/backend/apps/web/models/tags.py index eb33b31b1e..ef21ca08c9 100644 --- a/backend/apps/web/models/tags.py +++ b/backend/apps/web/models/tags.py @@ -15,7 +15,8 @@ from apps.web.internal.db import DB class Tag(Model): - name = CharField(unique=True) + id = CharField(unique=True) + name = CharField() user_id = CharField() data = TextField(null=True) @@ -24,7 +25,8 @@ class Tag(Model): class ChatIdTag(Model): - tag_name = ForeignKeyField(Tag, backref="chat_id_tags") + id = CharField(unique=True) + tag_name = CharField() chat_id = CharField() user_id = CharField() timestamp = DateField() @@ -34,12 +36,14 @@ class ChatIdTag(Model): class TagModel(BaseModel): + id: str name: str user_id: str data: Optional[str] = None class ChatIdTagModel(BaseModel): + id: str tag_name: str chat_id: str user_id: str @@ -70,14 +74,15 @@ class TagTable: db.create_tables([Tag, ChatIdTag]) def insert_new_tag(self, name: str, user_id: str) -> Optional[TagModel]: - tag = TagModel(**{"user_id": user_id, "name": name}) + id = str(uuid.uuid4()) + tag = TagModel(**{"id": id, "user_id": user_id, "name": name}) try: result = Tag.create(**tag.model_dump()) if result: return tag else: return None - except: + except Exception as e: return None def get_tag_by_name_and_user_id( @@ -86,17 +91,27 @@ class TagTable: try: tag = Tag.get(Tag.name == name, Tag.user_id == user_id) return TagModel(**model_to_dict(tag)) - except: + except Exception as e: return None def add_tag_to_chat( self, user_id: str, form_data: ChatIdTagForm - ) -> Optional[ChatTagsResponse]: + ) -> Optional[ChatIdTagModel]: tag = self.get_tag_by_name_and_user_id(form_data.tag_name, user_id) if tag == None: tag = self.insert_new_tag(form_data.tag_name, user_id) - chatIdTag = ChatIdTagModel(**{"user_id": user_id, "tag_name": tag.name}) + print(tag) + id = str(uuid.uuid4()) + chatIdTag = ChatIdTagModel( + **{ + "id": id, + "user_id": user_id, + "chat_id": form_data.chat_id, + "tag_name": tag.name, + "timestamp": int(time.time()), + } + ) try: result = ChatIdTag.create(**chatIdTag.model_dump()) if result: @@ -109,19 +124,17 @@ class TagTable: def get_tags_by_chat_id_and_user_id( self, chat_id: str, user_id: str ) -> List[TagModel]: + tag_names = [ + ChatIdTagModel(**model_to_dict(chat_id_tag)).tag_name + for chat_id_tag in ChatIdTag.select() + .where((ChatIdTag.user_id == user_id) & (ChatIdTag.chat_id == chat_id)) + .order_by(ChatIdTag.timestamp.desc()) + ] + + print(tag_names) return [ TagModel(**model_to_dict(tag)) - for tag in Tag.select().where( - Tag.name - in [ - ChatIdTagModel(**model_to_dict(chat_id_tag)).tag_name - for chat_id_tag in ChatIdTag.select() - .where( - (ChatIdTag.user_id == user_id) & (ChatIdTag.chat_id == chat_id) - ) - .order_by(ChatIdTag.timestamp.desc()) - ] - ) + for tag in Tag.select().where(Tag.name.in_(tag_names)) ] def get_chat_ids_by_tag_name_and_user_id( @@ -152,7 +165,8 @@ class TagTable: & (ChatIdTag.chat_id == chat_id) & (ChatIdTag.user_id == user_id) ) - query.execute() # Remove the rows, return number of rows removed. + res = query.execute() # Remove the rows, return number of rows removed. + print(res) tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id) if tag_count == 0: @@ -163,7 +177,8 @@ class TagTable: query.execute() # Remove the rows, return number of rows removed. return True - except: + except Exception as e: + print("delete_tag", e) return False def delete_tags_by_chat_id_and_user_id(self, chat_id: str, user_id: str) -> bool: diff --git a/backend/apps/web/routers/chats.py b/backend/apps/web/routers/chats.py index 0c9aa57367..38685826f6 100644 --- a/backend/apps/web/routers/chats.py +++ b/backend/apps/web/routers/chats.py @@ -19,6 +19,7 @@ from apps.web.models.chats import ( from apps.web.models.tags import ( TagModel, + ChatIdTagModel, ChatIdTagForm, ChatTagsResponse, Tags, @@ -132,7 +133,8 @@ async def delete_chat_by_id(id: str, user=Depends(get_current_user)): async def get_chat_tags_by_id(id: str, user=Depends(get_current_user)): tags = Tags.get_tags_by_chat_id_and_user_id(id, user.id) - if tags: + if tags != None: + print(tags) return tags else: raise HTTPException( @@ -145,17 +147,25 @@ async def get_chat_tags_by_id(id: str, user=Depends(get_current_user)): ############################ -@router.post("/{id}/tags", response_model=Optional[ChatTagsResponse]) +@router.post("/{id}/tags", response_model=Optional[ChatIdTagModel]) async def add_chat_tag_by_id( id: str, form_data: ChatIdTagForm, user=Depends(get_current_user) ): - tag = Tags.add_tag_to_chat(user.id, {"tag_name": form_data.tag_name, "chat_id": id}) + tags = Tags.get_tags_by_chat_id_and_user_id(id, user.id) - if tag: - return tag + if form_data.tag_name not in tags: + tag = Tags.add_tag_to_chat(user.id, form_data) + + if tag: + return tag + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) else: raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND + status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT() ) diff --git a/src/lib/components/layout/Navbar.svelte b/src/lib/components/layout/Navbar.svelte index 521e7abcc2..5899d58222 100644 --- a/src/lib/components/layout/Navbar.svelte +++ b/src/lib/components/layout/Navbar.svelte @@ -12,22 +12,11 @@ export let title: string = 'Ollama Web UI'; export let shareEnabled: boolean = false; - let showShareChatModal = false; + export let tags = []; + export let addTag: Function; + export let deleteTag: Function; - let tags = [ - // { - // name: 'general' - // }, - // { - // name: 'medicine' - // }, - // { - // name: 'cooking' - // }, - // { - // name: 'education' - // } - ]; + let showShareChatModal = false; let tagName = ''; let showTagInput = false; @@ -74,16 +63,17 @@ saveAs(blob, `chat-${chat.title}.txt`); }; - const addTag = () => { - if (!tags.find((e) => e.name === tagName)) { - tags = [ - ...tags, - { - name: JSON.parse(JSON.stringify(tagName)) - } - ]; - } + const addTagHandler = () => { + // if (!tags.find((e) => e.name === tagName)) { + // tags = [ + // ...tags, + // { + // name: JSON.parse(JSON.stringify(tagName)) + // } + // ]; + // } + addTag(tagName); tagName = ''; showTagInput = false; }; @@ -126,48 +116,19 @@
-
- {#each tags as tag} -
-
- {tag.name} -
- -
- {/each} - -
- {#if showTagInput} -
- - +
+ {tag.name} +
+ {/each} - - {/if} - - -
-
- {#if shareEnabled} + +
+ + + {/if} + + +
+
+ + {#each $tags as tag} + + {/each} +
+ {/if} +
{#each $chats.filter((chat) => { if (search === '') { diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index c7d8f5e624..7880235cf6 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -10,6 +10,7 @@ export const theme = writable('dark'); export const chatId = writable(''); export const chats = writable([]); +export const tags = writable([]); export const models = writable([]); export const modelfiles = writable([]); diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 39ae0eeac5..c7839d93f0 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -20,7 +20,8 @@ models, modelfiles, prompts, - documents + documents, + tags } from '$lib/stores'; import { REQUIRED_OLLAMA_VERSION, WEBUI_API_BASE_URL } from '$lib/constants'; @@ -29,6 +30,7 @@ import { checkVersion } from '$lib/utils'; import ShortcutsModal from '$lib/components/chat/ShortcutsModal.svelte'; import { getDocs } from '$lib/apis/documents'; + import { getAllChatTags } from '$lib/apis/chats'; let ollamaVersion = ''; let loaded = false; @@ -106,6 +108,7 @@ await modelfiles.set(await getModelfiles(localStorage.token)); await prompts.set(await getPrompts(localStorage.token)); await documents.set(await getDocs(localStorage.token)); + await tags.set(await getAllChatTags(localStorage.token)); modelfiles.subscribe(async () => { // should fetch models diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte index 29e4f2018b..9507579c61 100644 --- a/src/routes/(app)/+page.svelte +++ b/src/routes/(app)/+page.svelte @@ -6,7 +6,16 @@ import { goto } from '$app/navigation'; import { page } from '$app/stores'; - import { models, modelfiles, user, settings, chats, chatId, config } from '$lib/stores'; + import { + models, + modelfiles, + user, + settings, + chats, + chatId, + config, + tags as _tags + } from '$lib/stores'; import { copyToClipboard, splitStream } from '$lib/utils'; import { generateChatCompletion, cancelChatCompletion, generateTitle } from '$lib/apis/ollama'; @@ -14,6 +23,7 @@ addTagById, createNewChat, deleteTagById, + getAllChatTags, getChatList, getTagsById, updateChatById @@ -695,6 +705,8 @@ chat = await updateChatById(localStorage.token, $chatId, { tags: tags }); + + _tags.set(await getAllChatTags(localStorage.token)); }; const deleteTag = async (tagName) => { @@ -704,6 +716,8 @@ chat = await updateChatById(localStorage.token, $chatId, { tags: tags }); + + _tags.set(await getAllChatTags(localStorage.token)); }; const setChatTitle = async (_chatId, _title) => { diff --git a/src/routes/(app)/c/[id]/+page.svelte b/src/routes/(app)/c/[id]/+page.svelte index 37f6f39c41..206b739891 100644 --- a/src/routes/(app)/c/[id]/+page.svelte +++ b/src/routes/(app)/c/[id]/+page.svelte @@ -6,7 +6,16 @@ import { goto } from '$app/navigation'; import { page } from '$app/stores'; - import { models, modelfiles, user, settings, chats, chatId, config } from '$lib/stores'; + import { + models, + modelfiles, + user, + settings, + chats, + chatId, + config, + tags as _tags + } from '$lib/stores'; import { copyToClipboard, splitStream, convertMessagesToHistory } from '$lib/utils'; import { generateChatCompletion, generateTitle } from '$lib/apis/ollama'; @@ -14,6 +23,7 @@ addTagById, createNewChat, deleteTagById, + getAllChatTags, getChatById, getChatList, getTagsById, @@ -709,8 +719,10 @@ tags = await getTags(); chat = await updateChatById(localStorage.token, $chatId, { - tags: tags.map((tag) => tag.name) + tags: tags }); + + _tags.set(await getAllChatTags(localStorage.token)); }; const deleteTag = async (tagName) => { @@ -718,8 +730,10 @@ tags = await getTags(); chat = await updateChatById(localStorage.token, $chatId, { - tags: tags.map((tag) => tag.name) + tags: tags }); + + _tags.set(await getAllChatTags(localStorage.token)); }; onMount(async () => { From 76529acced2449b992f8a6a21d5f4f57287ec1ce Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Thu, 18 Jan 2024 02:57:31 -0800 Subject: [PATCH 085/398] doc: feature --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index dfa7c1a5c1..3a14b00aa6 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ Also check our sibling project, [OllamaHub](https://ollamahub.com/), where you c - πŸ‘πŸ‘Ž **RLHF Annotation**: Empower your messages by rating them with thumbs up and thumbs down, facilitating the creation of datasets for Reinforcement Learning from Human Feedback (RLHF). Utilize your messages to train or fine-tune models, all while ensuring the confidentiality of locally saved data. +- 🏷️ **Conversation Tagging**: Effortlessly categorize and locate specific chats for quick reference and streamlined data collection. + - πŸ“₯πŸ—‘οΈ **Download/Delete Models**: Easily download or remove models directly from the web UI. - ⬆️ **GGUF File Model Creation**: Effortlessly create Ollama models by uploading GGUF files directly from the web UI. Streamlined process with options to upload from your machine or download GGUF files from Hugging Face. From 1a06b0cea6151804e54f782e7dc17ededcb3e94c Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Thu, 18 Jan 2024 16:38:47 -0800 Subject: [PATCH 086/398] fix: old chat log import issue --- src/lib/components/chat/SettingsModal.svelte | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/lib/components/chat/SettingsModal.svelte b/src/lib/components/chat/SettingsModal.svelte index c79fe10680..bd80cd598a 100644 --- a/src/lib/components/chat/SettingsModal.svelte +++ b/src/lib/components/chat/SettingsModal.svelte @@ -142,13 +142,20 @@ importChats(chats); }; - reader.readAsText(importFiles[0]); + if (importFiles.length > 0) { + reader.readAsText(importFiles[0]); + } } const importChats = async (_chats) => { for (const chat of _chats) { console.log(chat); - await createNewChat(localStorage.token, chat.chat); + + if (chat.chat) { + await createNewChat(localStorage.token, chat.chat); + } else { + await createNewChat(localStorage.token, chat); + } } await chats.set(await getChatList(localStorage.token)); From aa1d386042cc6e30dea92ddd99684eecf7edc7c1 Mon Sep 17 00:00:00 2001 From: Marclass Date: Thu, 18 Jan 2024 20:41:14 -0700 Subject: [PATCH 087/398] Allow any file to be used for RAG. Changed RAG parser to prefer file extensions over MIME content types. If the type of file is not recognized assume it's a text file. --- backend/apps/rag/main.py | 56 +++++++-------------- src/lib/components/chat/MessageInput.svelte | 8 +-- src/routes/(app)/documents/+page.svelte | 6 ++- 3 files changed, 27 insertions(+), 43 deletions(-) diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py index 36a12e4cc7..65dde89adc 100644 --- a/backend/apps/rag/main.py +++ b/backend/apps/rag/main.py @@ -144,37 +144,21 @@ def store_doc( # "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm" print(file.content_type) - if file.content_type not in [ - "application/pdf", - "text/plain", - "text/csv", - "text/xml", - "text/x-python", - "text/css", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "application/octet-stream", - "application/x-javascript", - ]: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.FILE_NOT_SUPPORTED, - ) - text_xml=["text/xml"] + + text_xml=["xml"] octet_markdown=["md"] - octet_plain=[ + known_source_ext=[ "go", "py", "java", "sh", "bat", "ps1", "cmd", "js", "css", "cpp", "hpp","h", "c", "cs", "sql", "log", "ini", "pl" "pm", "r", "dart", "dockerfile", "env", "php", "hs", "hsc", "lua", "nginxconf", "conf", "m", "mm", "plsql", "perl", "rb", "rs", "db2", "scala", "bash", "swift", "vue", "svelte" ] + docx_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document" + known_doc_ext=["doc","docx"] file_ext=file.filename.split(".")[-1].lower() - if file.content_type == "application/octet-stream" and file_ext not in (octet_markdown + octet_plain): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.FILE_NOT_SUPPORTED, - ) - + known_type=True + try: filename = file.filename file_path = f"{UPLOAD_DIR}/{filename}" @@ -188,27 +172,22 @@ def store_doc( collection_name = calculate_sha256(f)[:63] f.close() - if file.content_type == "application/pdf": + if file_ext=="pdf": loader = PyPDFLoader(file_path) - elif ( - file.content_type - == "application/vnd.openxmlformats-officedocument.wordprocessingml.document" - ): + elif (file.content_type ==docx_type or file_ext in known_doc_ext): loader = Docx2txtLoader(file_path) - - elif file.content_type == "text/csv": + elif file_ext=="csv": loader = CSVLoader(file_path) - elif file.content_type in text_xml: + elif file_ext in text_xml: loader=UnstructuredXMLLoader(file_path) - elif file.content_type == "text/plain" or file.content_type.find("text/")>=0: + elif file_ext in known_source_ext or file.content_type.find("text/")>=0: loader = TextLoader(file_path) - elif file.content_type == "application/octet-stream": - if file_ext in octet_markdown: - loader = UnstructuredMarkdownLoader(file_path) - if file_ext in octet_plain: - loader = TextLoader(file_path) - elif file.content_type == "application/x-javascript": + elif file_ext in octet_markdown: + loader = UnstructuredMarkdownLoader(file_path) + else: loader = TextLoader(file_path) + known_type=False + data = loader.load() result = store_data_in_vector_db(data, collection_name) @@ -218,6 +197,7 @@ def store_doc( "status": True, "collection_name": collection_name, "filename": filename, + "known_type":known_type, } else: raise HTTPException( diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 4830b98c73..ff82d6069a 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -173,7 +173,8 @@ ) { uploadDoc(file); } else { - toast.error(`Unsupported File Type '${file['type']}'.`); + toast.error(`Unknown File Type '${file['type']}', but accepting and treating as plain text`); + uploadDoc(file); } } else { toast.error(`File not found.`); @@ -308,8 +309,9 @@ uploadDoc(file); filesInputElement.value = ''; } else { - toast.error(`Unsupported File Type '${file['type']}'.`); - inputFiles = null; + toast.error(`Unknown File Type '${file['type']}', but accepting and treating as plain text`); + uploadDoc(file); + filesInputElement.value = ''; } } else { toast.error(`File not found.`); diff --git a/src/routes/(app)/documents/+page.svelte b/src/routes/(app)/documents/+page.svelte index 9a0aa13011..597220b52c 100644 --- a/src/routes/(app)/documents/+page.svelte +++ b/src/routes/(app)/documents/+page.svelte @@ -73,7 +73,8 @@ ) { uploadDoc(file); } else { - toast.error(`Unsupported File Type '${file['type']}'.`); + toast.error(`Unknown File Type '${file['type']}', but accepting and treating as plain text`); + uploadDoc(file); } } else { toast.error(`File not found.`); @@ -153,7 +154,8 @@ ) { uploadDoc(file); } else { - toast.error(`Unsupported File Type '${file['type']}'.`); + toast.error(`Unknown File Type '${file['type']}', but accepting and treating as plain text`); + uploadDoc(file); } inputFiles = null; From f559068186f333ec6d73a87e693a950d9bf47442 Mon Sep 17 00:00:00 2001 From: Dave Bauman Date: Sat, 13 Jan 2024 08:46:56 -0500 Subject: [PATCH 088/398] feat: Add epub support --- Dockerfile | 5 +++++ backend/apps/rag/main.py | 17 +++++++++++++---- backend/constants.py | 2 ++ backend/requirements.txt | 3 ++- src/lib/components/chat/MessageInput.svelte | 18 ++++++++++++------ src/lib/constants.ts | 1 + 6 files changed, 35 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index fbfc9ae232..0fd149856c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,6 +28,11 @@ ENV WEBUI_JWT_SECRET_KEY "SECRET_KEY" WORKDIR /app +# Install pandoc +RUN apt-get update \ + && apt-get install -y pandoc \ + && rm -rf /var/lib/apt/lists/* + # copy embedding weight from build RUN mkdir -p /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2 COPY --from=build /app/onnx.tar.gz /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2 diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py index 65dde89adc..aa6906f05c 100644 --- a/backend/apps/rag/main.py +++ b/backend/apps/rag/main.py @@ -19,6 +19,7 @@ from langchain_community.document_loaders import ( PyPDFLoader, CSVLoader, Docx2txtLoader, + UnstructuredEPubLoader, UnstructuredWordDocumentLoader, UnstructuredMarkdownLoader, UnstructuredXMLLoader, @@ -184,6 +185,8 @@ def store_doc( loader = TextLoader(file_path) elif file_ext in octet_markdown: loader = UnstructuredMarkdownLoader(file_path) + elif file.content_type == "application/epub+zip": + loader = UnstructuredEPubLoader(file_path) else: loader = TextLoader(file_path) known_type=False @@ -206,10 +209,16 @@ def store_doc( ) except Exception as e: print(e) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT(e), - ) + if "No pandoc was found" in str(e): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.PANDOC_NOT_INSTALLED, + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) @app.get("/reset/db") diff --git a/backend/constants.py b/backend/constants.py index c9bfaec5a9..580db9c5f3 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -42,3 +42,5 @@ class ERROR_MESSAGES(str, Enum): USER_NOT_FOUND = "We could not find what you're looking for :/" API_KEY_NOT_FOUND = "Oops! It looks like there's a hiccup. The API key is missing. Please make sure to provide a valid API key to access this feature." MALICIOUS = "Unusual activities detected, please try again in a few minutes." + + PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance." diff --git a/backend/requirements.txt b/backend/requirements.txt index 76a2082499..c28fcb680b 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -24,8 +24,9 @@ pypdf docx2txt unstructured markdown +pypandoc PyJWT pyjwt[crypto] -black \ No newline at end of file +black diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index ff82d6069a..adf74561ae 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -121,13 +121,19 @@ error: '' }; - files = [...files, doc]; - const res = await uploadDocToVectorDB(localStorage.token, '', file); + try { + files = [...files, doc]; + const res = await uploadDocToVectorDB(localStorage.token, '', file); - if (res) { - doc.upload_status = true; - doc.collection_name = res.collection_name; - files = files; + if (res) { + doc.upload_status = true; + doc.collection_name = res.collection_name; + files = files; + } + } catch (e) { + // Remove the failed doc from the files array + files = files.filter((f) => f.name !== file.name); + toast.error(e); } }; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 1d54dae1ac..91d9dd59a5 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -12,6 +12,7 @@ export const WEB_UI_VERSION = 'v1.0.0-alpha-static'; export const REQUIRED_OLLAMA_VERSION = '0.1.16'; export const SUPPORTED_FILE_TYPE = [ + 'application/epub+zip', 'application/pdf', 'text/plain', 'text/csv', From 35ace5778494782943b10890a0cb90382c716ce8 Mon Sep 17 00:00:00 2001 From: Marclass Date: Fri, 19 Jan 2024 10:48:04 -0700 Subject: [PATCH 089/398] add rst document for RAG --- backend/apps/rag/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py index 65dde89adc..44cb78f3ac 100644 --- a/backend/apps/rag/main.py +++ b/backend/apps/rag/main.py @@ -22,6 +22,7 @@ from langchain_community.document_loaders import ( UnstructuredWordDocumentLoader, UnstructuredMarkdownLoader, UnstructuredXMLLoader, + UnstructuredRSTLoader, ) from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.vectorstores import Chroma @@ -178,6 +179,8 @@ def store_doc( loader = Docx2txtLoader(file_path) elif file_ext=="csv": loader = CSVLoader(file_path) + elif file_ext=="rst": + loader = UnstructuredRSTLoader(file_path, mode="elements") elif file_ext in text_xml: loader=UnstructuredXMLLoader(file_path) elif file_ext in known_source_ext or file.content_type.find("text/")>=0: From 4d85e2cb157f76bc61ecdfc2cdc42bf683ad4aa8 Mon Sep 17 00:00:00 2001 From: Brandon Hulston Date: Fri, 19 Jan 2024 11:22:28 -0700 Subject: [PATCH 090/398] Add validation for chatGPT imports, stopping any breaking issues when imports are corrupted/not compatible --- src/lib/utils/index.ts | 74 ++++++++++++++++++++++++++++++++---------- 1 file changed, 57 insertions(+), 17 deletions(-) diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index e7dfba2c75..b12bdedde3 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -206,25 +206,32 @@ const convertOpenAIMessages = (convo) => { const mapping = convo['mapping']; const messages = []; let currentId = ''; + let lastId = null; for (let message_id in mapping) { const message = mapping[message_id]; currentId = message_id; - if (message['message'] == null || message['message']['content']['parts'][0] == '') { - // Skip chat messages with no content - continue; - } else { - const new_chat = { - id: message_id, - parentId: messages.length > 0 && message['parent'] in mapping ? message['parent'] : null, - childrenIds: message['children'] || [], - role: message['message']?.['author']?.['role'] !== 'user' ? 'assistant' : 'user', - content: message['message']?.['content']?.['parts']?.[0] || '', - model: 'gpt-3.5-turbo', - done: true, - context: null - }; - messages.push(new_chat); + try { + if (messages.length == 0 && (message['message'] == null || + (message['message']['content']['parts']?.[0] == '' && message['message']['content']['text'] == null))) { + // Skip chat messages with no content + continue; + } else { + const new_chat = { + id: message_id, + parentId: lastId, + childrenIds: message['children'] || [], + role: message['message']?.['author']?.['role'] !== 'user' ? 'assistant' : 'user', + content: message['message']?.['content']?.['parts']?.[0] || message['message']?.['content']?.['text'] || '', + model: 'gpt-3.5-turbo', + done: true, + context: null + }; + messages.push(new_chat); + lastId = currentId; + } + } catch (error) { + console.log("Error with", message, "\nError:", error); } } @@ -245,13 +252,45 @@ const convertOpenAIMessages = (convo) => { return chat; }; +const validateChat = (chat) => { + // Because ChatGPT sometimes has features we can't use like DALL-E or migh have corrupted messages, need to validate + const messages = chat.messages; + + // Check if messages array is empty + if (messages.length === 0) { + return false; + } + + // Last message's children should be an empty array + const lastMessage = messages[messages.length - 1]; + if (lastMessage.childrenIds.length !== 0) { + return false; + } + + // First message's parent should be null + const firstMessage = messages[0]; + if (firstMessage.parentId !== null) { + return false; + } + + // Every message's content should be a string + for (let message of messages) { + if (typeof message.content !== 'string') { + return false; + } + } + + return true; +}; + export const convertOpenAIChats = (_chats) => { // Create a list of dictionaries with each conversation from import const chats = []; + let failed = 0; for (let convo of _chats) { const chat = convertOpenAIMessages(convo); - if (Object.keys(chat.history.messages).length > 0) { + if (validateChat(chat)) { chats.push({ id: convo['id'], user_id: '', @@ -259,7 +298,8 @@ export const convertOpenAIChats = (_chats) => { chat: chat, timestamp: convo['timestamp'] }); - } + } else { failed ++} } + console.log(failed, "Conversations could not be imported"); return chats; }; From e3503d66173bd6b9f89e3054a7e7ac4f2bde91df Mon Sep 17 00:00:00 2001 From: lucasew Date: Fri, 19 Jan 2024 17:12:14 -0300 Subject: [PATCH 091/398] backend: make dotenv optional Signed-off-by: lucasew --- backend/config.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/config.py b/backend/config.py index 2a96d018b6..f31ee16aaf 100644 --- a/backend/config.py +++ b/backend/config.py @@ -1,4 +1,3 @@ -from dotenv import load_dotenv, find_dotenv import os @@ -14,7 +13,11 @@ from constants import ERROR_MESSAGES from pathlib import Path -load_dotenv(find_dotenv("../.env")) +try: + from dotenv import load_dotenv, find_dotenv + load_dotenv(find_dotenv("../.env")) +except ImportError: + print("dotenv not installed, skipping...") #################################### From 5b26d2a686d47822d5959e90a1d0b64ebec88d67 Mon Sep 17 00:00:00 2001 From: lucasew Date: Fri, 19 Jan 2024 17:13:09 -0300 Subject: [PATCH 092/398] backend: make the data directory and the artifacts from the frontend customizable using environment variables Signed-off-by: lucasew --- backend/apps/web/internal/db.py | 4 +++- backend/apps/web/routers/utils.py | 12 +++++------- backend/config.py | 10 ++++++---- backend/main.py | 4 ++-- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/backend/apps/web/internal/db.py b/backend/apps/web/internal/db.py index 3d639f3c8e..802d2a4082 100644 --- a/backend/apps/web/internal/db.py +++ b/backend/apps/web/internal/db.py @@ -1,4 +1,6 @@ from peewee import * +from config import DATA_DIR -DB = SqliteDatabase("./data/ollama.db") + +DB = SqliteDatabase(str(DATA_DIR / "ollama.db")) DB.connect() diff --git a/backend/apps/web/routers/utils.py b/backend/apps/web/routers/utils.py index b2ca409af6..cb316bbcb3 100644 --- a/backend/apps/web/routers/utils.py +++ b/backend/apps/web/routers/utils.py @@ -11,7 +11,7 @@ import json from utils.misc import calculate_sha256 -from config import OLLAMA_API_BASE_URL +from config import OLLAMA_API_BASE_URL, DATA_DIR, UPLOAD_DIR from constants import ERROR_MESSAGES @@ -96,8 +96,7 @@ async def download( file_name = parse_huggingface_url(url) if file_name: - os.makedirs("./uploads", exist_ok=True) - file_path = os.path.join("./uploads", f"{file_name}") + file_path = str(UPLOAD_DIR / file_name) return StreamingResponse( download_file_stream(url, file_path, file_name), @@ -109,16 +108,15 @@ async def download( @router.post("/upload") def upload(file: UploadFile = File(...)): - os.makedirs("./data/uploads", exist_ok=True) - file_path = os.path.join("./data/uploads", file.filename) + file_path = UPLOAD_DIR / file.filename # Save file in chunks - with open(file_path, "wb+") as f: + with file_path.open("wb+") as f: for chunk in file.file: f.write(chunk) def file_process_stream(): - total_size = os.path.getsize(file_path) + total_size = os.path.getsize(str(file_path)) chunk_size = 1024 * 1024 try: with open(file_path, "rb") as f: diff --git a/backend/config.py b/backend/config.py index f31ee16aaf..188dbbd7ec 100644 --- a/backend/config.py +++ b/backend/config.py @@ -24,10 +24,12 @@ except ImportError: # File Upload #################################### +DATA_DIR = Path(os.getenv("DATA_DIR", './data')).resolve() -UPLOAD_DIR = "./data/uploads" -Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True) +UPLOAD_DIR = DATA_DIR / "uploads" +UPLOAD_DIR.mkdir(parents=True, exist_ok=True) +WEB_DIR = Path(os.getenv("WEB_DIR", "../build")) #################################### # ENV (dev,test,prod) @@ -82,10 +84,10 @@ if WEBUI_AUTH and WEBUI_JWT_SECRET_KEY == "": # RAG #################################### -CHROMA_DATA_PATH = "./data/vector_db" +CHROMA_DATA_PATH = DATA_DIR / "vector_db" EMBED_MODEL = "all-MiniLM-L6-v2" CHROMA_CLIENT = chromadb.PersistentClient( - path=CHROMA_DATA_PATH, settings=Settings(allow_reset=True) + path=str(CHROMA_DATA_PATH), settings=Settings(allow_reset=True) ) CHUNK_SIZE = 1500 CHUNK_OVERLAP = 100 diff --git a/backend/main.py b/backend/main.py index e4d4bdb57e..4b734da8d5 100644 --- a/backend/main.py +++ b/backend/main.py @@ -14,7 +14,7 @@ from apps.openai.main import app as openai_app from apps.web.main import app as webui_app from apps.rag.main import app as rag_app -from config import ENV +from config import ENV, WEB_DIR class SPAStaticFiles(StaticFiles): @@ -58,4 +58,4 @@ app.mount("/openai/api", openai_app) app.mount("/rag/api/v1", rag_app) -app.mount("/", SPAStaticFiles(directory="../build", html=True), name="spa-static-files") +app.mount("/", SPAStaticFiles(directory=str(WEB_DIR), html=True), name="spa-static-files") From 8662437a9f7d06b4fc6d22713e472428636f7562 Mon Sep 17 00:00:00 2001 From: Aditya Pratap Singh Date: Sat, 20 Jan 2024 04:17:06 +0530 Subject: [PATCH 093/398] Add workaround for gpt-4-vision-preview model --- backend/apps/openai/main.py | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/backend/apps/openai/main.py b/backend/apps/openai/main.py index ef9330c55f..cbf3043a6a 100644 --- a/backend/apps/openai/main.py +++ b/backend/apps/openai/main.py @@ -84,9 +84,37 @@ async def proxy(path: str, request: Request, user=Depends(get_current_user)): raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) - body = await request.body() - # headers = dict(request.headers) - # print(headers) + # Try to decode the body of the request from bytes to a UTF-8 string (Require add max_token to fix gpt-4-vision) + try: + body_str = (await request.body()).decode('utf-8') + except UnicodeDecodeError as e: + print("Error decoding request body:", e) + raise HTTPException(status_code=400, detail="Invalid request body") + # Check if the body is not empty + if body_str: + try: + + body_dict = json.loads(body_str) + except json.JSONDecodeError as e: + print("Error loading request body into a dictionary:", e) + raise HTTPException(status_code=400, detail="Invalid JSON in request body") + + # Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 10000 + # This is a workaround until OpenAI fixes the issue with this model + if body_dict.get("model") == "gpt-4-vision-preview": + body_dict["max_tokens"] = 10000 + print("Modified body_dict:", body_dict) + + # Try to convert the modified body back to JSON + try: + # Convert the modified body back to JSON + body_json = json.dumps(body_dict) + except TypeError as e: + print("Error converting modified body to JSON:", e) + raise HTTPException(status_code=500, detail="Internal server error") + else: + body_json = body_str # If the body is empty, use it as is + headers = {} headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}" @@ -96,7 +124,7 @@ async def proxy(path: str, request: Request, user=Depends(get_current_user)): r = requests.request( method=request.method, url=target_url, - data=body, + data=body_json, headers=headers, stream=True, ) From 60afd6ecddf4eae0808f193a6c146eb378ba076b Mon Sep 17 00:00:00 2001 From: Aditya Pratap Singh Date: Sat, 20 Jan 2024 04:34:47 +0530 Subject: [PATCH 094/398] Add workaround for gpt-4-vision-preview model that support 4k tokens --- backend/apps/openai/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/apps/openai/main.py b/backend/apps/openai/main.py index cbf3043a6a..1544949e53 100644 --- a/backend/apps/openai/main.py +++ b/backend/apps/openai/main.py @@ -99,10 +99,10 @@ async def proxy(path: str, request: Request, user=Depends(get_current_user)): print("Error loading request body into a dictionary:", e) raise HTTPException(status_code=400, detail="Invalid JSON in request body") - # Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 10000 + # Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000 # This is a workaround until OpenAI fixes the issue with this model if body_dict.get("model") == "gpt-4-vision-preview": - body_dict["max_tokens"] = 10000 + body_dict["max_tokens"] = 4000 print("Modified body_dict:", body_dict) # Try to convert the modified body back to JSON From 6a63c94153dd76f41857019530ef0054913b5cdd Mon Sep 17 00:00:00 2001 From: Shiyinq Date: Sat, 20 Jan 2024 21:54:53 +0700 Subject: [PATCH 095/398] feat: add guard clause to improve signup process --- backend/apps/web/routers/auths.py | 66 +++++++++++++++---------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/backend/apps/web/routers/auths.py b/backend/apps/web/routers/auths.py index f245601dcb..a0772223f7 100644 --- a/backend/apps/web/routers/auths.py +++ b/backend/apps/web/routers/auths.py @@ -91,42 +91,40 @@ async def signin(form_data: SigninForm): @router.post("/signup", response_model=SigninResponse) async def signup(request: Request, form_data: SignupForm): - if request.app.state.ENABLE_SIGNUP: - if validate_email_format(form_data.email.lower()): - if not Users.get_user_by_email(form_data.email.lower()): - try: - role = "admin" if Users.get_num_users() == 0 else "pending" - hashed = get_password_hash(form_data.password) - user = Auths.insert_new_auth(form_data.email.lower(), - hashed, form_data.name, role) - - if user: - token = create_token(data={"email": user.email}) - # response.set_cookie(key='token', value=token, httponly=True) - - return { - "token": token, - "token_type": "Bearer", - "id": user.id, - "email": user.email, - "name": user.name, - "role": user.role, - "profile_image_url": user.profile_image_url, - } - else: - raise HTTPException( - 500, detail=ERROR_MESSAGES.CREATE_USER_ERROR) - except Exception as err: - raise HTTPException(500, - detail=ERROR_MESSAGES.DEFAULT(err)) - else: - raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) - else: - raise HTTPException(400, - detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT) - else: + if not request.app.state.ENABLE_SIGNUP: raise HTTPException(400, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + + if not validate_email_format(form_data.email.lower()): + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT) + + if Users.get_user_by_email(form_data.email.lower()): + raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) + + try: + role = "admin" if Users.get_num_users() == 0 else "pending" + hashed = get_password_hash(form_data.password) + user = Auths.insert_new_auth(form_data.email.lower(), + hashed, form_data.name, role) + if user: + token = create_token(data={"email": user.email}) + # response.set_cookie(key='token', value=token, httponly=True) + + return { + "token": token, + "token_type": "Bearer", + "id": user.id, + "email": user.email, + "name": user.name, + "role": user.role, + "profile_image_url": user.profile_image_url, + } + else: + raise HTTPException( + 500, detail=ERROR_MESSAGES.CREATE_USER_ERROR) + except Exception as err: + raise HTTPException(500, + detail=ERROR_MESSAGES.DEFAULT(err)) ############################ # ToggleSignUp From b26e0fb7e707e202b42a8e3e5fc5067c26968f00 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Mon, 22 Jan 2024 01:37:54 -0800 Subject: [PATCH 096/398] refac --- backend/apps/openai/main.py | 70 +++++++++++++------------------------ 1 file changed, 25 insertions(+), 45 deletions(-) diff --git a/backend/apps/openai/main.py b/backend/apps/openai/main.py index 1544949e53..fd6acee719 100644 --- a/backend/apps/openai/main.py +++ b/backend/apps/openai/main.py @@ -37,19 +37,16 @@ async def get_openai_url(user=Depends(get_current_user)): if user and user.role == "admin": return {"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL} else: - raise HTTPException(status_code=401, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) @app.post("/url/update") -async def update_openai_url(form_data: UrlUpdateForm, - user=Depends(get_current_user)): +async def update_openai_url(form_data: UrlUpdateForm, user=Depends(get_current_user)): if user and user.role == "admin": app.state.OPENAI_API_BASE_URL = form_data.url return {"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL} else: - raise HTTPException(status_code=401, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) @app.get("/key") @@ -57,19 +54,16 @@ async def get_openai_key(user=Depends(get_current_user)): if user and user.role == "admin": return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY} else: - raise HTTPException(status_code=401, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) @app.post("/key/update") -async def update_openai_key(form_data: KeyUpdateForm, - user=Depends(get_current_user)): +async def update_openai_key(form_data: KeyUpdateForm, user=Depends(get_current_user)): if user and user.role == "admin": app.state.OPENAI_API_KEY = form_data.key return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY} else: - raise HTTPException(status_code=401, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) @@ -78,43 +72,29 @@ async def proxy(path: str, request: Request, user=Depends(get_current_user)): print(target_url, app.state.OPENAI_API_KEY) if user.role not in ["user", "admin"]: - raise HTTPException(status_code=401, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) if app.state.OPENAI_API_KEY == "": - raise HTTPException(status_code=401, - detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) + raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) + body = await request.body() + + # TODO: Remove below after gpt-4-vision fix from Open AI # Try to decode the body of the request from bytes to a UTF-8 string (Require add max_token to fix gpt-4-vision) try: - body_str = (await request.body()).decode('utf-8') - except UnicodeDecodeError as e: - print("Error decoding request body:", e) - raise HTTPException(status_code=400, detail="Invalid request body") - # Check if the body is not empty - if body_str: - try: - - body_dict = json.loads(body_str) - except json.JSONDecodeError as e: - print("Error loading request body into a dictionary:", e) - raise HTTPException(status_code=400, detail="Invalid JSON in request body") - + body = body.decode("utf-8") + body = json.loads(body) + # Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000 # This is a workaround until OpenAI fixes the issue with this model - if body_dict.get("model") == "gpt-4-vision-preview": - body_dict["max_tokens"] = 4000 - print("Modified body_dict:", body_dict) - - # Try to convert the modified body back to JSON - try: - # Convert the modified body back to JSON - body_json = json.dumps(body_dict) - except TypeError as e: - print("Error converting modified body to JSON:", e) - raise HTTPException(status_code=500, detail="Internal server error") - else: - body_json = body_str # If the body is empty, use it as is + if body.get("model") == "gpt-4-vision-preview": + body["max_tokens"] = 4000 + print("Modified body_dict:", body) + # Convert the modified body back to JSON + body = json.dumps(body) + except json.JSONDecodeError as e: + print("Error loading request body into a dictionary:", e) + raise HTTPException(status_code=400, detail="Invalid JSON in request body") headers = {} headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}" @@ -124,7 +104,7 @@ async def proxy(path: str, request: Request, user=Depends(get_current_user)): r = requests.request( method=request.method, url=target_url, - data=body_json, + data=body, headers=headers, stream=True, ) @@ -153,8 +133,8 @@ async def proxy(path: str, request: Request, user=Depends(get_current_user)): if "openai" in app.state.OPENAI_API_BASE_URL and path == "models": response_data["data"] = list( - filter(lambda model: "gpt" in model["id"], - response_data["data"])) + filter(lambda model: "gpt" in model["id"], response_data["data"]) + ) return response_data except Exception as e: From 83181b7968b462ad5e218802273213b6fa6ca3ab Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Mon, 22 Jan 2024 01:41:00 -0800 Subject: [PATCH 097/398] fix: add max_token only when field not present --- backend/apps/openai/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/apps/openai/main.py b/backend/apps/openai/main.py index fd6acee719..1529ab93f8 100644 --- a/backend/apps/openai/main.py +++ b/backend/apps/openai/main.py @@ -87,7 +87,8 @@ async def proxy(path: str, request: Request, user=Depends(get_current_user)): # Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000 # This is a workaround until OpenAI fixes the issue with this model if body.get("model") == "gpt-4-vision-preview": - body["max_tokens"] = 4000 + if "max_tokens" not in body: + body["max_tokens"] = 4000 print("Modified body_dict:", body) # Convert the modified body back to JSON From d2c5f3d59164137e047f2d93f207b6f8f7a44cd3 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Mon, 22 Jan 2024 01:47:07 -0800 Subject: [PATCH 098/398] refac: convert str var to f-string --- backend/apps/web/internal/db.py | 2 +- backend/apps/web/routers/utils.py | 4 ++-- backend/config.py | 9 +++++---- backend/main.py | 8 ++++++-- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/backend/apps/web/internal/db.py b/backend/apps/web/internal/db.py index 802d2a4082..1f8c3bf7d2 100644 --- a/backend/apps/web/internal/db.py +++ b/backend/apps/web/internal/db.py @@ -2,5 +2,5 @@ from peewee import * from config import DATA_DIR -DB = SqliteDatabase(str(DATA_DIR / "ollama.db")) +DB = SqliteDatabase(f"{DATA_DIR}/ollama.db") DB.connect() diff --git a/backend/apps/web/routers/utils.py b/backend/apps/web/routers/utils.py index cb316bbcb3..0bb3ea3c6f 100644 --- a/backend/apps/web/routers/utils.py +++ b/backend/apps/web/routers/utils.py @@ -96,7 +96,7 @@ async def download( file_name = parse_huggingface_url(url) if file_name: - file_path = str(UPLOAD_DIR / file_name) + file_path = f"{UPLOAD_DIR}/{file_name}" return StreamingResponse( download_file_stream(url, file_path, file_name), @@ -108,7 +108,7 @@ async def download( @router.post("/upload") def upload(file: UploadFile = File(...)): - file_path = UPLOAD_DIR / file.filename + file_path = f"{UPLOAD_DIR}/{file.filename}" # Save file in chunks with file_path.open("wb+") as f: diff --git a/backend/config.py b/backend/config.py index 188dbbd7ec..59ef194735 100644 --- a/backend/config.py +++ b/backend/config.py @@ -15,6 +15,7 @@ from pathlib import Path try: from dotenv import load_dotenv, find_dotenv + load_dotenv(find_dotenv("../.env")) except ImportError: print("dotenv not installed, skipping...") @@ -24,12 +25,12 @@ except ImportError: # File Upload #################################### -DATA_DIR = Path(os.getenv("DATA_DIR", './data')).resolve() +DATA_DIR = Path(os.getenv("DATA_DIR", "./data")).resolve() -UPLOAD_DIR = DATA_DIR / "uploads" +UPLOAD_DIR = f"{DATA_DIR}/uploads" UPLOAD_DIR.mkdir(parents=True, exist_ok=True) -WEB_DIR = Path(os.getenv("WEB_DIR", "../build")) +FRONTEND_BUILD_DIR = Path(os.getenv("FRONTEND_BUILD_DIR", "../build")) #################################### # ENV (dev,test,prod) @@ -84,7 +85,7 @@ if WEBUI_AUTH and WEBUI_JWT_SECRET_KEY == "": # RAG #################################### -CHROMA_DATA_PATH = DATA_DIR / "vector_db" +CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db" EMBED_MODEL = "all-MiniLM-L6-v2" CHROMA_CLIENT = chromadb.PersistentClient( path=str(CHROMA_DATA_PATH), settings=Settings(allow_reset=True) diff --git a/backend/main.py b/backend/main.py index 4b734da8d5..a0ad73fd64 100644 --- a/backend/main.py +++ b/backend/main.py @@ -14,7 +14,7 @@ from apps.openai.main import app as openai_app from apps.web.main import app as webui_app from apps.rag.main import app as rag_app -from config import ENV, WEB_DIR +from config import ENV, FRONTEND_BUILD_DIR class SPAStaticFiles(StaticFiles): @@ -58,4 +58,8 @@ app.mount("/openai/api", openai_app) app.mount("/rag/api/v1", rag_app) -app.mount("/", SPAStaticFiles(directory=str(WEB_DIR), html=True), name="spa-static-files") +app.mount( + "/", + SPAStaticFiles(directory=str(FRONTEND_BUILD_DIR), html=True), + name="spa-static-files", +) From e758855590524cad590b415d19658cd0168a5e9e Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Mon, 22 Jan 2024 03:33:49 -0800 Subject: [PATCH 099/398] refac: response message --- .../components/chat/Messages/CodeBlock.svelte | 36 ++++++ .../chat/Messages/ResponseMessage.svelte | 103 ++++++------------ 2 files changed, 67 insertions(+), 72 deletions(-) create mode 100644 src/lib/components/chat/Messages/CodeBlock.svelte diff --git a/src/lib/components/chat/Messages/CodeBlock.svelte b/src/lib/components/chat/Messages/CodeBlock.svelte new file mode 100644 index 0000000000..a8df6bfa47 --- /dev/null +++ b/src/lib/components/chat/Messages/CodeBlock.svelte @@ -0,0 +1,36 @@ + + +
+
+
{lang}
+ +
+ +
{@html highlightedCode || code}
+
diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte index 37a593f162..21dafed5da 100644 --- a/src/lib/components/chat/Messages/ResponseMessage.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -1,17 +1,16 @@ -
+
@@ -30,7 +30,7 @@ >
-
{@html highlightedCode || code}
From 917ab08f5c001ad9d3fe007cae9876ac92f1738d Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Mon, 22 Jan 2024 03:43:48 -0800 Subject: [PATCH 101/398] fix: code block styling --- src/lib/components/chat/Messages/CodeBlock.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/chat/Messages/CodeBlock.svelte b/src/lib/components/chat/Messages/CodeBlock.svelte index df50104e83..c93485c43b 100644 --- a/src/lib/components/chat/Messages/CodeBlock.svelte +++ b/src/lib/components/chat/Messages/CodeBlock.svelte @@ -22,7 +22,7 @@
{lang}
+
-
+
{@html lang}
+ +
-
{@html highlightedCode || code}
-
+
{@html highlightedCode || code}
+
+{/if} diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte index 21dafed5da..ef88207f20 100644 --- a/src/lib/components/chat/Messages/ResponseMessage.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -154,328 +154,164 @@ }); -
- +{#key message.id} +
+ -
- - {#if message.model in modelfiles} - {modelfiles[message.model]?.title} +
+ + {#if message.model in modelfiles} + {modelfiles[message.model]?.title} + {:else} + Ollama {message.model ? ` ${message.model}` : ''} + {/if} + + {#if message.timestamp} + + {/if} + + + {#if message.content === ''} + {:else} - Ollama {message.model ? ` ${message.model}` : ''} - {/if} - - {#if message.timestamp} - - {/if} - - - {#if message.content === ''} - - {:else} -
-
- {#if edit === true} -
-