From f9f3fe5ec02ef63259e1ad3ac2fa1de7335f9e54 Mon Sep 17 00:00:00 2001 From: Danny Liu Date: Sat, 2 Mar 2024 23:23:49 -0800 Subject: [PATCH 01/30] refac delete logic --- src/lib/components/chat/Messages.svelte | 132 +++++++++++------------- src/routes/(app)/+page.svelte | 4 +- src/routes/(app)/c/[id]/+page.svelte | 4 +- 3 files changed, 64 insertions(+), 76 deletions(-) diff --git a/src/lib/components/chat/Messages.svelte b/src/lib/components/chat/Messages.svelte index fd0a6e835..77e6cad0d 100644 --- a/src/lib/components/chat/Messages.svelte +++ b/src/lib/components/chat/Messages.svelte @@ -223,32 +223,22 @@ }, 100); }; - // TODO: change delete behaviour - // const deleteMessageAndDescendants = async (messageId: string) => { - // if (history.messages[messageId]) { - // history.messages[messageId].deleted = true; - - // for (const childId of history.messages[messageId].childrenIds) { - // await deleteMessageAndDescendants(childId); - // } - // } - // }; - - // const triggerDeleteMessageRecursive = async (messageId: string) => { - // await deleteMessageAndDescendants(messageId); - // await updateChatById(localStorage.token, chatId, { history }); - // await chats.set(await getChatList(localStorage.token)); - // }; - const messageDeleteHandler = async (messageId) => { - if (history.messages[messageId]) { - history.messages[messageId].deleted = true; + const messageParentId = history.messages[messageId]?.parentId; - for (const childId of history.messages[messageId].childrenIds) { - history.messages[childId].deleted = true; - } + if (messageParentId !== null) { + history.messages[messageParentId].childrenIds = [] } - await updateChatById(localStorage.token, chatId, { history }); + + delete history.messages[messageId]; + history.currentId = messageParentId; + + await tick(); + + await updateChatById(localStorage.token, chatId, { + messages: messages, + history: history + }); }; @@ -258,57 +248,55 @@
{#key chatId} {#each messages as message, messageIdx} - {#if !message.deleted} -
-
- {#if message.role === 'user'} - messageDeleteHandler(message.id)} - user={$user} - {message} - isFirstMessage={messageIdx === 0} - siblings={message.parentId !== null - ? history.messages[message.parentId]?.childrenIds ?? [] - : Object.values(history.messages) - .filter((message) => message.parentId === null) - .map((message) => message.id) ?? []} - {confirmEditMessage} - {showPreviousMessage} - {showNextMessage} - {copyToClipboard} - /> - {:else} - { - console.log('save', e); +
+
+ {#if message.role === 'user'} + messageDeleteHandler(message.id)} + user={$user} + {message} + isFirstMessage={messageIdx === 0} + siblings={message.parentId !== null + ? history.messages[message.parentId]?.childrenIds ?? [] + : Object.values(history.messages) + .filter((message) => message.parentId === null) + .map((message) => message.id) ?? []} + {confirmEditMessage} + {showPreviousMessage} + {showNextMessage} + {copyToClipboard} + /> + {:else} + { + console.log('save', e); - const message = e.detail; - history.messages[message.id] = message; - await updateChatById(localStorage.token, chatId, { - messages: messages, - history: history - }); - }} - /> - {/if} -
+ const message = e.detail; + history.messages[message.id] = message; + await updateChatById(localStorage.token, chatId, { + messages: messages, + history: history + }); + }} + /> + {/if}
- {/if} +
{/each} {#if bottomPadding} diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte index 04dc922c9..da3aff237 100644 --- a/src/routes/(app)/+page.svelte +++ b/src/routes/(app)/+page.svelte @@ -342,7 +342,7 @@ content: $settings.system } : undefined, - ...messages.filter((message) => !message.deleted) + ...messages ] .filter((message) => message) .map((message, idx, arr) => ({ @@ -550,7 +550,7 @@ content: $settings.system } : undefined, - ...messages.filter((message) => !message.deleted) + ...messages ] .filter((message) => message) .map((message, idx, arr) => ({ diff --git a/src/routes/(app)/c/[id]/+page.svelte b/src/routes/(app)/c/[id]/+page.svelte index 69ebfde55..85638c401 100644 --- a/src/routes/(app)/c/[id]/+page.svelte +++ b/src/routes/(app)/c/[id]/+page.svelte @@ -355,7 +355,7 @@ content: $settings.system } : undefined, - ...messages.filter((message) => !message.deleted) + ...messages ] .filter((message) => message) .map((message, idx, arr) => ({ @@ -563,7 +563,7 @@ content: $settings.system } : undefined, - ...messages.filter((message) => !message.deleted) + ...messages ] .filter((message) => message) .map((message, idx, arr) => ({ From 225038738294f35228003ba497d3378c0aaa3a8e Mon Sep 17 00:00:00 2001 From: Danny Liu Date: Sat, 2 Mar 2024 23:31:03 -0800 Subject: [PATCH 02/30] retain the message in history object --- src/lib/components/chat/Messages.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/components/chat/Messages.svelte b/src/lib/components/chat/Messages.svelte index 77e6cad0d..5711fb174 100644 --- a/src/lib/components/chat/Messages.svelte +++ b/src/lib/components/chat/Messages.svelte @@ -230,7 +230,6 @@ history.messages[messageParentId].childrenIds = [] } - delete history.messages[messageId]; history.currentId = messageParentId; await tick(); From 8c011974834dcb2f5b4d72bc6e22843e647bc3b5 Mon Sep 17 00:00:00 2001 From: Danny Liu Date: Mon, 4 Mar 2024 12:34:14 -0800 Subject: [PATCH 03/30] refac: reassign grandchildren of deleted message as children of parent message --- src/lib/components/chat/Messages.svelte | 32 ++++++++++++++++++++----- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/lib/components/chat/Messages.svelte b/src/lib/components/chat/Messages.svelte index 5711fb174..3f1ee28b6 100644 --- a/src/lib/components/chat/Messages.svelte +++ b/src/lib/components/chat/Messages.svelte @@ -224,15 +224,35 @@ }; const messageDeleteHandler = async (messageId) => { - const messageParentId = history.messages[messageId]?.parentId; + const messageToDelete = history.messages[messageId]; + const messageParentId = messageToDelete.parentId; + const messageChildrenIds = messageToDelete.childrenIds ?? []; - if (messageParentId !== null) { - history.messages[messageParentId].childrenIds = [] - } + messageChildrenIds.forEach((childId) => { + const child = history.messages[childId]; + if (child && child.childrenIds) { + if (child.childrenIds.length == 0) { // if last prompt/response pair + history.messages[messageParentId].childrenIds = [] + history.currentId = messageParentId; + } + else { + child.childrenIds.forEach((grandChildId) => { + if (history.messages[grandChildId]) { + history.messages[grandChildId].parentId = messageParentId; + history.messages[messageParentId].childrenIds.push(grandChildId); + } + }); + } + } - history.currentId = messageParentId; + // remove response + history.messages[messageParentId].childrenIds = history.messages[messageParentId].childrenIds + .filter((id) => id !== childId); + }); - await tick(); + // remove prompt + history.messages[messageParentId].childrenIds = history.messages[messageParentId].childrenIds + .filter((id) => id !== messageId); await updateChatById(localStorage.token, chatId, { messages: messages, From 03907f9a8adde951a855e665bc61e6fbc265a42a Mon Sep 17 00:00:00 2001 From: Danny Liu Date: Mon, 4 Mar 2024 21:23:41 -0800 Subject: [PATCH 04/30] check if deleted message response has sibling responses for multi model chats --- src/lib/components/chat/Messages.svelte | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/components/chat/Messages.svelte b/src/lib/components/chat/Messages.svelte index 3f1ee28b6..2ce107bb1 100644 --- a/src/lib/components/chat/Messages.svelte +++ b/src/lib/components/chat/Messages.svelte @@ -228,10 +228,12 @@ const messageParentId = messageToDelete.parentId; const messageChildrenIds = messageToDelete.childrenIds ?? []; + const hasSibling = messageChildrenIds.some(childId => history.messages[childId]?.childrenIds?.length > 0); + messageChildrenIds.forEach((childId) => { const child = history.messages[childId]; if (child && child.childrenIds) { - if (child.childrenIds.length == 0) { // if last prompt/response pair + if (child.childrenIds.length === 0 && !hasSibling) { // if last prompt/response pair history.messages[messageParentId].childrenIds = [] history.currentId = messageParentId; } From 95b20f3cb805b2a5a659e818e08ab5ad5690d900 Mon Sep 17 00:00:00 2001 From: Jannik S <69747628+jannikstdl@users.noreply.github.com> Date: Tue, 5 Mar 2024 16:45:47 +0100 Subject: [PATCH 05/30] feat: show latest changes in releases This should show the latest Changes in the CHANGELOG.md file when creating a new release. For now it only shows "Automatically created new release". It is running 'awk '/^## [/{n++} n==1' CHANGELOG.md' which extracts the latest changes. Note: I only tested the command on my linux machine, worked. --- .github/workflows/build-release.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index fa3fa296d..e45a83164 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -19,24 +19,34 @@ jobs: echo "No changes to package.json" exit 1 } - + - name: Get version number from package.json id: get_version run: | VERSION=$(jq -r '.version' package.json) echo "::set-output name=version::$VERSION" + - name: Extract latest CHANGELOG entry + id: changelog + run: | + CHANGELOG_CONTENT=$(awk '/^## \[/{n++} n==1' CHANGELOG.md) + echo "CHANGELOG_CONTENT< Date: Tue, 5 Mar 2024 22:25:25 +0100 Subject: [PATCH 06/30] feat: added ocr functionality to the pdf loader --- backend/apps/rag/main.py | 2 +- backend/requirements.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py index 2a8b2a49e..ee07d51a5 100644 --- a/backend/apps/rag/main.py +++ b/backend/apps/rag/main.py @@ -419,7 +419,7 @@ def get_loader(filename: str, file_content_type: str, file_path: str): ] if file_ext == "pdf": - loader = PyPDFLoader(file_path) + loader = PyPDFLoader(file_path, extract_images=True) elif file_ext == "csv": loader = CSVLoader(file_path) elif file_ext == "rst": diff --git a/backend/requirements.txt b/backend/requirements.txt index 0cacacd80..9de3df961 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -33,6 +33,7 @@ pandas openpyxl pyxlsb xlrd +rapidocr-onnxruntime faster-whisper From a9d74b669e71d5db989f8766f9535620f20238c5 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Wed, 6 Mar 2024 00:33:47 -0800 Subject: [PATCH 07/30] refac --- src/lib/components/chat/Messages.svelte | 48 ++++++++----------------- 1 file changed, 15 insertions(+), 33 deletions(-) diff --git a/src/lib/components/chat/Messages.svelte b/src/lib/components/chat/Messages.svelte index 2ce107bb1..481c89674 100644 --- a/src/lib/components/chat/Messages.svelte +++ b/src/lib/components/chat/Messages.svelte @@ -224,42 +224,24 @@ }; const messageDeleteHandler = async (messageId) => { - const messageToDelete = history.messages[messageId]; - const messageParentId = messageToDelete.parentId; - const messageChildrenIds = messageToDelete.childrenIds ?? []; + const message = history.messages[messageId]; + const parentId = message.parentId; + const childrenIds = message.childrenIds ?? []; + const grandchildrenIds = []; - const hasSibling = messageChildrenIds.some(childId => history.messages[childId]?.childrenIds?.length > 0); + // Iterate through childrenIds to find grandchildrenIds + for (const childId of childrenIds) { + const childMessage = history.messages[childId]; + const grandChildrenIds = childMessage.childrenIds ?? []; + grandchildrenIds.push(...grandChildrenIds); + } - messageChildrenIds.forEach((childId) => { - const child = history.messages[childId]; - if (child && child.childrenIds) { - if (child.childrenIds.length === 0 && !hasSibling) { // if last prompt/response pair - history.messages[messageParentId].childrenIds = [] - history.currentId = messageParentId; - } - else { - child.childrenIds.forEach((grandChildId) => { - if (history.messages[grandChildId]) { - history.messages[grandChildId].parentId = messageParentId; - history.messages[messageParentId].childrenIds.push(grandChildId); - } - }); - } - } + history.messages[parentId].childrenIds.push(...grandchildrenIds); + history.messages[parentId].childrenIds = history.messages[parentId].childrenIds.filter( + (id) => id !== messageId + ); - // remove response - history.messages[messageParentId].childrenIds = history.messages[messageParentId].childrenIds - .filter((id) => id !== childId); - }); - - // remove prompt - history.messages[messageParentId].childrenIds = history.messages[messageParentId].childrenIds - .filter((id) => id !== messageId); - - await updateChatById(localStorage.token, chatId, { - messages: messages, - history: history - }); + await updateChatById(localStorage.token, chatId, { messages, history }); }; From 2b016adc6e27913108a3de5591c9c054f1fb61c4 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Wed, 6 Mar 2024 00:55:51 -0800 Subject: [PATCH 08/30] revert --- src/lib/components/chat/Messages.svelte | 88 ++++++++++++++++++++----- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/src/lib/components/chat/Messages.svelte b/src/lib/components/chat/Messages.svelte index 481c89674..339d88608 100644 --- a/src/lib/components/chat/Messages.svelte +++ b/src/lib/components/chat/Messages.svelte @@ -224,25 +224,79 @@ }; const messageDeleteHandler = async (messageId) => { - const message = history.messages[messageId]; - const parentId = message.parentId; - const childrenIds = message.childrenIds ?? []; - const grandchildrenIds = []; - - // Iterate through childrenIds to find grandchildrenIds - for (const childId of childrenIds) { - const childMessage = history.messages[childId]; - const grandChildrenIds = childMessage.childrenIds ?? []; - grandchildrenIds.push(...grandChildrenIds); - } - - history.messages[parentId].childrenIds.push(...grandchildrenIds); - history.messages[parentId].childrenIds = history.messages[parentId].childrenIds.filter( - (id) => id !== messageId + const messageToDelete = history.messages[messageId]; + const messageParentId = messageToDelete.parentId; + const messageChildrenIds = messageToDelete.childrenIds ?? []; + const hasSibling = messageChildrenIds.some( + (childId) => history.messages[childId]?.childrenIds?.length > 0 ); - - await updateChatById(localStorage.token, chatId, { messages, history }); + messageChildrenIds.forEach((childId) => { + const child = history.messages[childId]; + if (child && child.childrenIds) { + if (child.childrenIds.length === 0 && !hasSibling) { + // if last prompt/response pair + history.messages[messageParentId].childrenIds = []; + history.currentId = messageParentId; + } else { + child.childrenIds.forEach((grandChildId) => { + if (history.messages[grandChildId]) { + history.messages[grandChildId].parentId = messageParentId; + history.messages[messageParentId].childrenIds.push(grandChildId); + } + }); + } + } + // remove response + history.messages[messageParentId].childrenIds = history.messages[ + messageParentId + ].childrenIds.filter((id) => id !== childId); + }); + // remove prompt + history.messages[messageParentId].childrenIds = history.messages[ + messageParentId + ].childrenIds.filter((id) => id !== messageId); + await updateChatById(localStorage.token, chatId, { + messages: messages, + history: history + }); }; + + // const messageDeleteHandler = async (messageId) => { + // const message = history.messages[messageId]; + // const parentId = message.parentId; + // const childrenIds = message.childrenIds ?? []; + // const grandchildrenIds = []; + + // // Iterate through childrenIds to find grandchildrenIds + // for (const childId of childrenIds) { + // const childMessage = history.messages[childId]; + // const grandChildrenIds = childMessage.childrenIds ?? []; + + // for (const grandchildId of grandchildrenIds) { + // const childMessage = history.messages[grandchildId]; + // childMessage.parentId = parentId; + // } + // grandchildrenIds.push(...grandChildrenIds); + // } + + // history.messages[parentId].childrenIds.push(...grandchildrenIds); + // history.messages[parentId].childrenIds = history.messages[parentId].childrenIds.filter( + // (id) => id !== messageId + // ); + + // // Select latest message + // let currentMessageId = grandchildrenIds.at(-1); + // if (currentMessageId) { + // let messageChildrenIds = history.messages[currentMessageId].childrenIds; + // while (messageChildrenIds.length !== 0) { + // currentMessageId = messageChildrenIds.at(-1); + // messageChildrenIds = history.messages[currentMessageId].childrenIds; + // } + // history.currentId = currentMessageId; + // } + + // await updateChatById(localStorage.token, chatId, { messages, history }); + // }; {#if messages.length == 0} From e4eb32709d39a6b4dcec2dd61c0fd6afbadab7b4 Mon Sep 17 00:00:00 2001 From: Justin Hayes Date: Wed, 6 Mar 2024 11:08:04 -0500 Subject: [PATCH 09/30] Remove extra 'S' in 'ASSISTANT' --- src/lib/components/chat/Settings/Models.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/chat/Settings/Models.svelte b/src/lib/components/chat/Settings/Models.svelte index 70bc4d2e9..84c7fefd7 100644 --- a/src/lib/components/chat/Settings/Models.svelte +++ b/src/lib/components/chat/Settings/Models.svelte @@ -45,7 +45,7 @@ let modelUploadMode = 'file'; let modelInputFile = ''; let modelFileUrl = ''; - let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop ""\nPARAMETER stop "USER:"\nPARAMETER stop "ASSSISTANT:"`; + let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop ""\nPARAMETER stop "USER:"\nPARAMETER stop "ASSSISTANT:"`; let modelFileDigest = ''; let uploadProgress = null; From 7258a794f19e8e0d6ae27d095c4b2ff517567630 Mon Sep 17 00:00:00 2001 From: Justin Hayes Date: Wed, 6 Mar 2024 12:59:47 -0500 Subject: [PATCH 10/30] Remove colons and another extra 'S' --- src/lib/components/chat/Settings/Models.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/chat/Settings/Models.svelte b/src/lib/components/chat/Settings/Models.svelte index 84c7fefd7..9169a63cf 100644 --- a/src/lib/components/chat/Settings/Models.svelte +++ b/src/lib/components/chat/Settings/Models.svelte @@ -45,7 +45,7 @@ let modelUploadMode = 'file'; let modelInputFile = ''; let modelFileUrl = ''; - let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop ""\nPARAMETER stop "USER:"\nPARAMETER stop "ASSSISTANT:"`; + let modelFileContent = `TEMPLATE """{{ .System }}\nUSER {{ .Prompt }}\nASSISTANT """\nPARAMETER num_ctx 4096\nPARAMETER stop ""\nPARAMETER stop "USER"\nPARAMETER stop "ASSISTANT"`; let modelFileDigest = ''; let uploadProgress = null; From 0aa4e3aafb6e8227e6dcf1f255f921cf9141a731 Mon Sep 17 00:00:00 2001 From: Justin Hayes Date: Wed, 6 Mar 2024 13:03:39 -0500 Subject: [PATCH 11/30] Restore colons --- src/lib/components/chat/Settings/Models.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/chat/Settings/Models.svelte b/src/lib/components/chat/Settings/Models.svelte index 9169a63cf..e540885f5 100644 --- a/src/lib/components/chat/Settings/Models.svelte +++ b/src/lib/components/chat/Settings/Models.svelte @@ -45,7 +45,7 @@ let modelUploadMode = 'file'; let modelInputFile = ''; let modelFileUrl = ''; - let modelFileContent = `TEMPLATE """{{ .System }}\nUSER {{ .Prompt }}\nASSISTANT """\nPARAMETER num_ctx 4096\nPARAMETER stop ""\nPARAMETER stop "USER"\nPARAMETER stop "ASSISTANT"`; + let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop ""\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`; let modelFileDigest = ''; let uploadProgress = null; From 534ba9e4f0a4ae6c862fef3a558bca3ece080844 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Wed, 6 Mar 2024 11:09:04 -0800 Subject: [PATCH 12/30] fix: message edit textarea height --- src/lib/components/chat/Messages/ResponseMessage.svelte | 1 + src/lib/components/chat/Messages/UserMessage.svelte | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte index f1237f771..a52b0967d 100644 --- a/src/lib/components/chat/Messages/ResponseMessage.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -344,6 +344,7 @@ class=" bg-transparent outline-none w-full resize-none" bind:value={editedContent} on:input={(e) => { + e.target.style.height = ''; e.target.style.height = `${e.target.scrollHeight}px`; }} /> diff --git a/src/lib/components/chat/Messages/UserMessage.svelte b/src/lib/components/chat/Messages/UserMessage.svelte index 18c9b6700..d5e8b81fd 100644 --- a/src/lib/components/chat/Messages/UserMessage.svelte +++ b/src/lib/components/chat/Messages/UserMessage.svelte @@ -168,7 +168,8 @@ class=" bg-transparent outline-none w-full resize-none" bind:value={editedContent} on:input={(e) => { - messageEditTextAreaElement.style.height = `${messageEditTextAreaElement.scrollHeight}px`; + e.target.style.height = ''; + e.target.style.height = `${e.target.scrollHeight}px`; }} /> From 434595b3e6a638e6b704eefd3fcffba21951988d Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Wed, 6 Mar 2024 11:14:33 -0800 Subject: [PATCH 13/30] fix: delete model list --- src/lib/components/chat/Settings/Models.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/chat/Settings/Models.svelte b/src/lib/components/chat/Settings/Models.svelte index c99a9c638..391dc0084 100644 --- a/src/lib/components/chat/Settings/Models.svelte +++ b/src/lib/components/chat/Settings/Models.svelte @@ -507,7 +507,7 @@ {#if !deleteModelTag} {/if} - {#each $models.filter((m) => m.size != null) as model} + {#each $models.filter((m) => m.size != null && (selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx))) as model} From 5ae121b08776cb7f6b8563b5defa70cc2f29d75b Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Wed, 6 Mar 2024 11:26:39 -0800 Subject: [PATCH 14/30] feat: message tooltips --- .../chat/Messages/ResponseMessage.svelte | 521 +++++++++--------- .../chat/Messages/UserMessage.svelte | 103 ++-- 2 files changed, 328 insertions(+), 296 deletions(-) diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte index a52b0967d..6e29a4ca5 100644 --- a/src/lib/components/chat/Messages/ResponseMessage.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -22,6 +22,7 @@ import CodeBlock from './CodeBlock.svelte'; import Image from '$lib/components/common/Image.svelte'; import { WEBUI_BASE_URL } from '$lib/constants'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; export let modelfiles = []; export let message; @@ -463,189 +464,125 @@
{/if} - - - - - - - - - - {#if $config.images} + + + + + + + + + + + + + + + + + + + + {#if $config.images} + + + {/if} {#if message.info} - + + + + + {/if} {#if isLastMessage} - + + + + + + - + + + + + {/if}
{/if} diff --git a/src/lib/components/chat/Messages/UserMessage.svelte b/src/lib/components/chat/Messages/UserMessage.svelte index d5e8b81fd..45ae52b49 100644 --- a/src/lib/components/chat/Messages/UserMessage.svelte +++ b/src/lib/components/chat/Messages/UserMessage.svelte @@ -5,6 +5,7 @@ import Name from './Name.svelte'; import ProfileImage from './ProfileImage.svelte'; import { modelfiles, settings } from '$lib/stores'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; const dispatch = createEventDispatcher(); @@ -246,55 +247,11 @@ {/if} - - - - - {#if !isFirstMessage} + + + + + + + + {#if !isFirstMessage} + + + {/if} From f741adc6c952fc5db1ff62c6af770d77c82a0aba Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Wed, 6 Mar 2024 11:44:00 -0800 Subject: [PATCH 15/30] refac: OLLAMA_API_BASE_URL deprecated --- .env.example | 4 ++-- Dockerfile | 2 +- README.md | 6 +++--- TROUBLESHOOTING.md | 8 ++++---- backend/apps/ollama/main.py | 5 ++--- backend/apps/web/routers/utils.py | 6 +++--- backend/config.py | 17 +++++++++++------ docker-compose.yaml | 4 ++-- kubernetes/helm/templates/webui-deployment.yaml | 2 +- kubernetes/manifest/base/webui-deployment.yaml | 4 ++-- package.json | 2 +- src/lib/constants.ts | 5 ----- 12 files changed, 32 insertions(+), 33 deletions(-) diff --git a/.env.example b/.env.example index de763f31c..3d2aafc09 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ # Ollama URL for the backend to connect -# The path '/ollama/api' will be redirected to the specified backend URL -OLLAMA_API_BASE_URL='http://localhost:11434/api' +# The path '/ollama' will be redirected to the specified backend URL +OLLAMA_BASE_URL='http://localhost:11434' OPENAI_API_BASE_URL='' OPENAI_API_KEY='' diff --git a/Dockerfile b/Dockerfile index 7eb34dab8..2d2f522ae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ FROM python:3.11-slim-bookworm as base ENV ENV=prod ENV PORT "" -ENV OLLAMA_API_BASE_URL "/ollama/api" +ENV OLLAMA_BASE_URL "/ollama" ENV OPENAI_API_BASE_URL "" ENV OPENAI_API_KEY "" diff --git a/README.md b/README.md index 0b346853b..2528996e8 100644 --- a/README.md +++ b/README.md @@ -95,10 +95,10 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open - **If Ollama is on a Different Server**, use this command: -- To connect to Ollama on another server, change the `OLLAMA_API_BASE_URL` to the server's URL: +- To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL: ```bash - docker run -d -p 3000:8080 -e OLLAMA_API_BASE_URL=https://example.com/api -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main + docker run -d -p 3000:8080 -e OLLAMA_BASE_URL=https://example.com -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main ``` - After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! πŸ˜„ @@ -110,7 +110,7 @@ If you're experiencing connection issues, it’s often due to the WebUI docker c **Example Docker Command**: ```bash -docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_API_BASE_URL=http://127.0.0.1:11434/api --name open-webui --restart always ghcr.io/open-webui/open-webui:main +docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main ``` ### Other Installation Methods diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index d3163501a..8e8f89da0 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -4,7 +4,7 @@ The Open WebUI system is designed to streamline interactions between the client (your browser) and the Ollama API. At the heart of this design is a backend reverse proxy, enhancing security and resolving CORS issues. -- **How it Works**: The Open WebUI is designed to interact with the Ollama API through a specific route. When a request is made from the WebUI to Ollama, it is not directly sent to the Ollama API. Initially, the request is sent to the Open WebUI backend via `/ollama/api` route. From there, the backend is responsible for forwarding the request to the Ollama API. This forwarding is accomplished by using the route specified in the `OLLAMA_API_BASE_URL` environment variable. Therefore, a request made to `/ollama/api` in the WebUI is effectively the same as making a request to `OLLAMA_API_BASE_URL` in the backend. For instance, a request to `/ollama/api/tags` in the WebUI is equivalent to `OLLAMA_API_BASE_URL/tags` in the backend. +- **How it Works**: The Open WebUI is designed to interact with the Ollama API through a specific route. When a request is made from the WebUI to Ollama, it is not directly sent to the Ollama API. Initially, the request is sent to the Open WebUI backend via `/ollama` route. From there, the backend is responsible for forwarding the request to the Ollama API. This forwarding is accomplished by using the route specified in the `OLLAMA_BASE_URL` environment variable. Therefore, a request made to `/ollama` in the WebUI is effectively the same as making a request to `OLLAMA_BASE_URL` in the backend. For instance, a request to `/ollama/api/tags` in the WebUI is equivalent to `OLLAMA_BASE_URL/api/tags` in the backend. - **Security Benefits**: This design prevents direct exposure of the Ollama API to the frontend, safeguarding against potential CORS (Cross-Origin Resource Sharing) issues and unauthorized access. Requiring authentication to access the Ollama API further enhances this security layer. @@ -15,7 +15,7 @@ If you're experiencing connection issues, it’s often due to the WebUI docker c **Example Docker Command**: ```bash -docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_API_BASE_URL=http://127.0.0.1:11434/api --name open-webui --restart always ghcr.io/open-webui/open-webui:main +docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main ``` ### General Connection Errors @@ -25,8 +25,8 @@ docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_API_BASE_ **Troubleshooting Steps**: 1. **Verify Ollama URL Format**: - - When running the Web UI container, ensure the `OLLAMA_API_BASE_URL` is correctly set, including the `/api` suffix. (e.g., `http://192.168.1.1:11434/api` for different host setups). + - When running the Web UI container, ensure the `OLLAMA_BASE_URL` is correctly set. (e.g., `http://192.168.1.1:11434` for different host setups). - In the Open WebUI, navigate to "Settings" > "General". - - Confirm that the Ollama Server URL is correctly set to `[OLLAMA URL]/api` (e.g., `http://localhost:11434/api`), including the `/api` suffix. + - Confirm that the Ollama Server URL is correctly set to `[OLLAMA URL]` (e.g., `http://localhost:11434`). By following these enhanced troubleshooting steps, connection issues should be effectively resolved. For further assistance or queries, feel free to reach out to us on our community Discord. diff --git a/backend/apps/ollama/main.py b/backend/apps/ollama/main.py index d4d1e91a6..f4442d04f 100644 --- a/backend/apps/ollama/main.py +++ b/backend/apps/ollama/main.py @@ -15,7 +15,7 @@ import asyncio from apps.web.models.users import Users from constants import ERROR_MESSAGES from utils.utils import decode_token, get_current_user, get_admin_user -from config import OLLAMA_BASE_URL, WEBUI_AUTH +from config import OLLAMA_BASE_URLS from typing import Optional, List, Union @@ -29,8 +29,7 @@ app.add_middleware( allow_headers=["*"], ) -app.state.OLLAMA_BASE_URL = OLLAMA_BASE_URL -app.state.OLLAMA_BASE_URLS = [OLLAMA_BASE_URL] +app.state.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS app.state.MODELS = {} diff --git a/backend/apps/web/routers/utils.py b/backend/apps/web/routers/utils.py index 6356bf452..fbb350cf2 100644 --- a/backend/apps/web/routers/utils.py +++ b/backend/apps/web/routers/utils.py @@ -14,7 +14,7 @@ import json from utils.utils import get_admin_user from utils.misc import calculate_sha256, get_gravatar_url -from config import OLLAMA_API_BASE_URL, DATA_DIR, UPLOAD_DIR +from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR from constants import ERROR_MESSAGES @@ -75,7 +75,7 @@ async def download_file_stream(url, file_path, file_name, chunk_size=1024 * 1024 hashed = calculate_sha256(file) file.seek(0) - url = f"{OLLAMA_API_BASE_URL}/blobs/sha256:{hashed}" + url = f"{OLLAMA_BASE_URLS[0]}/blobs/sha256:{hashed}" response = requests.post(url, data=file) if response.ok: @@ -147,7 +147,7 @@ def upload(file: UploadFile = File(...)): hashed = calculate_sha256(f) f.seek(0) - url = f"{OLLAMA_API_BASE_URL}/blobs/sha256:{hashed}" + url = f"{OLLAMA_BASE_URLS[0]}/blobs/sha256:{hashed}" response = requests.post(url, data=f) if response.ok: diff --git a/backend/config.py b/backend/config.py index cd1a27023..f01989cb5 100644 --- a/backend/config.py +++ b/backend/config.py @@ -207,20 +207,25 @@ OLLAMA_API_BASE_URL = os.environ.get( "OLLAMA_API_BASE_URL", "http://localhost:11434/api" ) -if ENV == "prod": - if OLLAMA_API_BASE_URL == "/ollama/api": - OLLAMA_API_BASE_URL = "http://host.docker.internal:11434/api" - - OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "") -if OLLAMA_BASE_URL == "": +if ENV == "prod": + if OLLAMA_BASE_URL == "/ollama": + OLLAMA_BASE_URL = "http://host.docker.internal:11434" + + +if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "": OLLAMA_BASE_URL = ( OLLAMA_API_BASE_URL[:-4] if OLLAMA_API_BASE_URL.endswith("/api") else OLLAMA_API_BASE_URL ) +OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "") +OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != "" else OLLAMA_BASE_URL + +OLLAMA_BASE_URLS = [url.strip() for url in OLLAMA_BASE_URLS.split(",")] + #################################### # OPENAI_API diff --git a/docker-compose.yaml b/docker-compose.yaml index c41c56d8e..f69084b8a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -14,7 +14,7 @@ services: build: context: . args: - OLLAMA_API_BASE_URL: '/ollama/api' + OLLAMA_BASE_URL: '/ollama' dockerfile: Dockerfile image: ghcr.io/open-webui/open-webui:main container_name: open-webui @@ -25,7 +25,7 @@ services: ports: - ${OPEN_WEBUI_PORT-3000}:8080 environment: - - 'OLLAMA_API_BASE_URL=http://ollama:11434/api' + - 'OLLAMA_BASE_URL=http://ollama:11434' - 'WEBUI_SECRET_KEY=' extra_hosts: - host.docker.internal:host-gateway diff --git a/kubernetes/helm/templates/webui-deployment.yaml b/kubernetes/helm/templates/webui-deployment.yaml index df13a14b6..bbd5706de 100644 --- a/kubernetes/helm/templates/webui-deployment.yaml +++ b/kubernetes/helm/templates/webui-deployment.yaml @@ -40,7 +40,7 @@ spec: - name: data mountPath: /app/backend/data env: - - name: OLLAMA_API_BASE_URL + - name: OLLAMA_BASE_URL value: {{ include "ollama.url" . | quote }} tty: true {{- with .Values.webui.nodeSelector }} diff --git a/kubernetes/manifest/base/webui-deployment.yaml b/kubernetes/manifest/base/webui-deployment.yaml index 174025a94..38efd5549 100644 --- a/kubernetes/manifest/base/webui-deployment.yaml +++ b/kubernetes/manifest/base/webui-deployment.yaml @@ -26,8 +26,8 @@ spec: cpu: "1000m" memory: "1Gi" env: - - name: OLLAMA_API_BASE_URL - value: "http://ollama-service.open-webui.svc.cluster.local:11434/api" + - name: OLLAMA_BASE_URL + value: "http://ollama-service.open-webui.svc.cluster.local:11434" tty: true volumeMounts: - name: webui-volume diff --git a/package.json b/package.json index cb16b3476..980793992 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.1.108", + "version": "0.1.109", "private": true, "scripts": { "dev": "vite dev --host", diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 2042198fb..bdd9c64e9 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -90,8 +90,3 @@ export const SUPPORTED_FILE_EXTENSIONS = [ // This feature, akin to $env/static/private, exclusively incorporates environment variables // that are prefixed with config.kit.env.publicPrefix (usually set to PUBLIC_). // Consequently, these variables can be securely exposed to client-side code. - -// Example of the .env configuration: -// OLLAMA_API_BASE_URL="http://localhost:11434/api" -// # Public -// PUBLIC_API_BASE_URL=$OLLAMA_API_BASE_URL From c9173b71b6d76c8ffeaf5f70dbb18536b95b34f5 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Wed, 6 Mar 2024 11:51:51 -0800 Subject: [PATCH 16/30] chore: comment rename --- backend/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/config.py b/backend/config.py index f01989cb5..2b7cd7e69 100644 --- a/backend/config.py +++ b/backend/config.py @@ -200,7 +200,7 @@ if not os.path.exists(LITELLM_CONFIG_PATH): #################################### -# OLLAMA_API_BASE_URL +# OLLAMA_BASE_URL #################################### OLLAMA_API_BASE_URL = os.environ.get( From ad237b61f3531f368e77cef3d2f4ed77b13e0859 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Wed, 6 Mar 2024 12:15:47 -0800 Subject: [PATCH 17/30] Update CHANGELOG.md --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9e86fe8a..afbe67756 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.109] - 2024-03-06 + +### Added + +- **πŸ”„ Multiple Ollama Servers Support**: Enjoy enhanced scalability and performance with support for multiple Ollama servers in a single WebUI. Load balancing features are now available, providing improved efficiency (#788, #278). +- **πŸ”§ Support for Claude 3 and Gemini**: Responding to user requests, we've expanded our toolset to include Claude 3 and Gemini, offering a wider range of functionalities within our platform (#1064). +- **πŸ” OCR Functionality for PDF Loader**: We've augmented our PDF loader with Optical Character Recognition (OCR) capabilities. Now, extract text from scanned documents and images within PDFs, broadening the scope of content processing (#1050). + +### Fixed + +- **πŸ› οΈ RAG Collection**: Implemented a dynamic mechanism to recreate RAG collections, ensuring users have up-to-date and accurate data (#1031). +- **πŸ“ User Agent Headers**: Fixed issue of RAG web requests being sent with empty user_agent headers, reducing rejections from certain websites. Realistic headers are now utilized for these requests (#1024). +- **⏹️ Playground Cancel Functionality**: Introducing a new "Cancel" option for stopping Ollama generation in the Playground, enhancing user control and usability (#1006). +- **πŸ”€ Typographical Error in 'ASSISTANT' Field**: Corrected a typographical error in the 'ASSISTANT' field within the GGUF model upload template for accuracy and consistency (#1061). + +### Changed + +- **πŸ”„ Refactored Message Deletion Logic**: Streamlined message deletion process for improved efficiency and user experience, simplifying interactions within the platform (#1004). +- **⚠️ Deprecation of `OLLAMA_API_BASE_URL`**: Deprecated `OLLAMA_API_BASE_URL` environment variable; recommend using `OLLAMA_BASE_URL` instead. Refer to our documentation for further details. + ## [0.1.108] - 2024-03-02 ### Added From 0142c2de43cb808e93bcb20cc0a7b92e0e830189 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Wed, 6 Mar 2024 12:42:14 -0800 Subject: [PATCH 18/30] refac: semicolon as delimiter --- backend/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/config.py b/backend/config.py index 2b7cd7e69..8373dc12d 100644 --- a/backend/config.py +++ b/backend/config.py @@ -224,7 +224,7 @@ if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "": OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "") OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != "" else OLLAMA_BASE_URL -OLLAMA_BASE_URLS = [url.strip() for url in OLLAMA_BASE_URLS.split(",")] +OLLAMA_BASE_URLS = [url.strip() for url in OLLAMA_BASE_URLS.split(";")] #################################### From 51d509bafb78fd245a404d8b2dc1af25d335bf0d Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Wed, 6 Mar 2024 13:18:17 -0800 Subject: [PATCH 19/30] Update config.py --- backend/config.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/backend/config.py b/backend/config.py index 8373dc12d..264fab465 100644 --- a/backend/config.py +++ b/backend/config.py @@ -237,6 +237,19 @@ OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "") if OPENAI_API_BASE_URL == "": OPENAI_API_BASE_URL = "https://api.openai.com/v1" +OPENAI_API_KEYS = os.environ.get("OPENAI_API_KEYS", "") +OPENAI_API_KEYS = OPENAI_API_KEYS if OPENAI_API_KEYS != "" else OPENAI_API_KEY + +OPENAI_API_KEYS = [url.strip() for url in OPENAI_API_BASE_URL.split(";")] + + +OPENAI_API_BASE_URLS = os.environ.get("OPENAI_API_BASE_URLS", "") +OPENAI_API_BASE_URLS = ( + OPENAI_API_BASE_URLS if OPENAI_API_BASE_URLS != "" else OPENAI_API_BASE_URL +) + +OPENAI_API_BASE_URLS = [url.strip() for url in OPENAI_API_BASE_URL.split(";")] + #################################### # WEBUI From c255cba198c0d7de924890fa5cb5bc035e3ded7b Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Wed, 6 Mar 2024 16:13:25 -0800 Subject: [PATCH 20/30] feat: multiple openai apis --- backend/apps/openai/main.py | 271 ++++++++++++------ backend/config.py | 2 +- backend/constants.py | 2 + src/lib/apis/openai/index.ts | 28 +- .../chat/Settings/Connections.svelte | 114 +++++--- src/routes/(app)/+layout.svelte | 3 - 6 files changed, 277 insertions(+), 143 deletions(-) diff --git a/backend/apps/openai/main.py b/backend/apps/openai/main.py index 8d6fdb509..3ff0d68e1 100644 --- a/backend/apps/openai/main.py +++ b/backend/apps/openai/main.py @@ -3,7 +3,10 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse, JSONResponse, FileResponse import requests +import aiohttp +import asyncio import json + from pydantic import BaseModel @@ -15,7 +18,9 @@ from utils.utils import ( get_verified_user, get_admin_user, ) -from config import OPENAI_API_BASE_URL, OPENAI_API_KEY, CACHE_DIR +from config import OPENAI_API_BASE_URLS, OPENAI_API_KEYS, CACHE_DIR +from typing import List, Optional + import hashlib from pathlib import Path @@ -29,116 +34,208 @@ app.add_middleware( allow_headers=["*"], ) -app.state.OPENAI_API_BASE_URL = OPENAI_API_BASE_URL -app.state.OPENAI_API_KEY = OPENAI_API_KEY +app.state.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS +app.state.OPENAI_API_KEYS = OPENAI_API_KEYS + +app.state.MODELS = {} -class UrlUpdateForm(BaseModel): - url: str +@app.middleware("http") +async def check_url(request: Request, call_next): + if len(app.state.MODELS) == 0: + await get_all_models() + else: + pass + + response = await call_next(request) + return response -class KeyUpdateForm(BaseModel): - key: str +class UrlsUpdateForm(BaseModel): + urls: List[str] -@app.get("/url") -async def get_openai_url(user=Depends(get_admin_user)): - return {"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL} +class KeysUpdateForm(BaseModel): + keys: List[str] -@app.post("/url/update") -async def update_openai_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)): - app.state.OPENAI_API_BASE_URL = form_data.url - return {"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL} +@app.get("/urls") +async def get_openai_urls(user=Depends(get_admin_user)): + return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS} -@app.get("/key") -async def get_openai_key(user=Depends(get_admin_user)): - return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY} +@app.post("/urls/update") +async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)): + app.state.OPENAI_API_BASE_URLS = form_data.urls + return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS} -@app.post("/key/update") -async def update_openai_key(form_data: KeyUpdateForm, user=Depends(get_admin_user)): - app.state.OPENAI_API_KEY = form_data.key - return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY} +@app.get("/keys") +async def get_openai_keys(user=Depends(get_admin_user)): + return {"OPENAI_API_KEYS": app.state.OPENAI_API_KEYS} + + +@app.post("/keys/update") +async def update_openai_key(form_data: KeysUpdateForm, user=Depends(get_admin_user)): + app.state.OPENAI_API_KEYS = form_data.keys + return {"OPENAI_API_KEYS": app.state.OPENAI_API_KEYS} @app.post("/audio/speech") async def speech(request: Request, user=Depends(get_verified_user)): - target_url = f"{app.state.OPENAI_API_BASE_URL}/audio/speech" - - if app.state.OPENAI_API_KEY == "": - raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) - - body = await request.body() - - name = hashlib.sha256(body).hexdigest() - - SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/") - SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True) - file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3") - file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json") - - # Check if the file already exists in the cache - if file_path.is_file(): - return FileResponse(file_path) - - headers = {} - headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}" - headers["Content-Type"] = "application/json" - + idx = None try: - print("openai") - r = requests.post( - url=target_url, - data=body, - headers=headers, - stream=True, + idx = app.state.OPENAI_API_BASE_URLS.index("https://api.openai.com/v1") + body = await request.body() + name = hashlib.sha256(body).hexdigest() + + SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/") + SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True) + file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3") + file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json") + + # Check if the file already exists in the cache + if file_path.is_file(): + return FileResponse(file_path) + + headers = {} + headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEYS[idx]}" + headers["Content-Type"] = "application/json" + + try: + r = requests.post( + url=f"{app.state.OPENAI_API_BASE_URLS[idx]}/audio/speech", + data=body, + headers=headers, + stream=True, + ) + + r.raise_for_status() + + # Save the streaming content to a file + with open(file_path, "wb") as f: + for chunk in r.iter_content(chunk_size=8192): + f.write(chunk) + + with open(file_body_path, "w") as f: + json.dump(json.loads(body.decode("utf-8")), f) + + # Return the saved file + return FileResponse(file_path) + + except Exception as e: + print(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"External: {res['error']}" + except: + error_detail = f"External: {e}" + + raise HTTPException(status_code=r.status_code, detail=error_detail) + + except ValueError: + raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND) + + +async def fetch_url(url, key): + try: + headers = {"Authorization": f"Bearer {key}"} + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=headers) as response: + return await response.json() + except Exception as e: + # Handle connection error here + print(f"Connection error: {e}") + return None + + +def merge_models_lists(model_lists): + merged_list = [] + + for idx, models in enumerate(model_lists): + merged_list.extend( + [ + {**model, "urlIdx": idx} + for model in models + if "api.openai.com" not in app.state.OPENAI_API_BASE_URLS[idx] + or "gpt" in model["id"] + ] ) - r.raise_for_status() + return merged_list - # Save the streaming content to a file - with open(file_path, "wb") as f: - for chunk in r.iter_content(chunk_size=8192): - f.write(chunk) - with open(file_body_path, "w") as f: - json.dump(json.loads(body.decode("utf-8")), f) +async def get_all_models(): + print("get_all_models") + tasks = [ + fetch_url(f"{url}/models", app.state.OPENAI_API_KEYS[idx]) + for idx, url in enumerate(app.state.OPENAI_API_BASE_URLS) + ] + responses = await asyncio.gather(*tasks) + responses = list(filter(lambda x: x is not None, responses)) - # Return the saved file - return FileResponse(file_path) + models = { + "data": merge_models_lists( + list(map(lambda response: response["data"], responses)) + ) + } + app.state.MODELS = {model["id"]: model for model in models["data"]} - except Exception as e: - print(e) - error_detail = "Open WebUI: Server Connection Error" - if r is not None: - try: - res = r.json() - if "error" in res: - error_detail = f"External: {res['error']}" - except: - error_detail = f"External: {e}" + return models - raise HTTPException(status_code=r.status_code, detail=error_detail) + +# , user=Depends(get_current_user) +@app.get("/models") +@app.get("/models/{url_idx}") +async def get_models(url_idx: Optional[int] = None): + if url_idx == None: + return await get_all_models() + else: + url = app.state.OPENAI_API_BASE_URLS[url_idx] + try: + r = requests.request(method="GET", url=f"{url}/models") + r.raise_for_status() + + response_data = r.json() + if "api.openai.com" in url: + response_data["data"] = list( + filter(lambda model: "gpt" in model["id"], response_data["data"]) + ) + + return response_data + except Exception as e: + print(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"External: {res['error']}" + except: + error_detail = f"External: {e}" + + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, + ) @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) async def proxy(path: str, request: Request, user=Depends(get_verified_user)): - target_url = f"{app.state.OPENAI_API_BASE_URL}/{path}" - print(target_url, app.state.OPENAI_API_KEY) - - if app.state.OPENAI_API_KEY == "": - raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) + idx = 0 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 = body.decode("utf-8") body = json.loads(body) + idx = app.state.MODELS[body.get("model")]["urlIdx"] + # 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": @@ -158,8 +255,16 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): except json.JSONDecodeError as e: print("Error loading request body into a dictionary:", e) + url = app.state.OPENAI_API_BASE_URLS[idx] + key = app.state.OPENAI_API_KEYS[idx] + + target_url = f"{url}/{path}" + + if key == "": + raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) + headers = {} - headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}" + headers["Authorization"] = f"Bearer {key}" headers["Content-Type"] = "application/json" try: @@ -181,21 +286,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): headers=dict(r.headers), ) else: - # For non-SSE, read the response and return it - # response_data = ( - # r.json() - # if r.headers.get("Content-Type", "") - # == "application/json" - # else r.text - # ) - response_data = r.json() - - if "api.openai.com" in app.state.OPENAI_API_BASE_URL and path == "models": - response_data["data"] = list( - filter(lambda model: "gpt" in model["id"], response_data["data"]) - ) - return response_data except Exception as e: print(e) diff --git a/backend/config.py b/backend/config.py index 264fab465..1db4d98e5 100644 --- a/backend/config.py +++ b/backend/config.py @@ -240,7 +240,7 @@ if OPENAI_API_BASE_URL == "": OPENAI_API_KEYS = os.environ.get("OPENAI_API_KEYS", "") OPENAI_API_KEYS = OPENAI_API_KEYS if OPENAI_API_KEYS != "" else OPENAI_API_KEY -OPENAI_API_KEYS = [url.strip() for url in OPENAI_API_BASE_URL.split(";")] +OPENAI_API_KEYS = [url.strip() for url in OPENAI_API_KEYS.split(";")] OPENAI_API_BASE_URLS = os.environ.get("OPENAI_API_BASE_URLS", "") diff --git a/backend/constants.py b/backend/constants.py index b2bbe9aae..eacf8a20f 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -41,6 +41,7 @@ class ERROR_MESSAGES(str, Enum): NOT_FOUND = "We could not find what you're looking for :/" 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." @@ -50,3 +51,4 @@ class ERROR_MESSAGES(str, Enum): RATE_LIMIT_EXCEEDED = "API rate limit exceeded" MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found" + OPENAI_NOT_FOUND = lambda name="": f"OpenAI API was not found" diff --git a/src/lib/apis/openai/index.ts b/src/lib/apis/openai/index.ts index 3a629eb31..e38314a55 100644 --- a/src/lib/apis/openai/index.ts +++ b/src/lib/apis/openai/index.ts @@ -1,9 +1,9 @@ import { OPENAI_API_BASE_URL } from '$lib/constants'; -export const getOpenAIUrl = async (token: string = '') => { +export const getOpenAIUrls = async (token: string = '') => { let error = null; - const res = await fetch(`${OPENAI_API_BASE_URL}/url`, { + const res = await fetch(`${OPENAI_API_BASE_URL}/urls`, { method: 'GET', headers: { Accept: 'application/json', @@ -29,13 +29,13 @@ export const getOpenAIUrl = async (token: string = '') => { throw error; } - return res.OPENAI_API_BASE_URL; + return res.OPENAI_API_BASE_URLS; }; -export const updateOpenAIUrl = async (token: string = '', url: string) => { +export const updateOpenAIUrls = async (token: string = '', urls: string[]) => { let error = null; - const res = await fetch(`${OPENAI_API_BASE_URL}/url/update`, { + const res = await fetch(`${OPENAI_API_BASE_URL}/urls/update`, { method: 'POST', headers: { Accept: 'application/json', @@ -43,7 +43,7 @@ export const updateOpenAIUrl = async (token: string = '', url: string) => { ...(token && { authorization: `Bearer ${token}` }) }, body: JSON.stringify({ - url: url + urls: urls }) }) .then(async (res) => { @@ -64,13 +64,13 @@ export const updateOpenAIUrl = async (token: string = '', url: string) => { throw error; } - return res.OPENAI_API_BASE_URL; + return res.OPENAI_API_BASE_URLS; }; -export const getOpenAIKey = async (token: string = '') => { +export const getOpenAIKeys = async (token: string = '') => { let error = null; - const res = await fetch(`${OPENAI_API_BASE_URL}/key`, { + const res = await fetch(`${OPENAI_API_BASE_URL}/keys`, { method: 'GET', headers: { Accept: 'application/json', @@ -96,13 +96,13 @@ export const getOpenAIKey = async (token: string = '') => { throw error; } - return res.OPENAI_API_KEY; + return res.OPENAI_API_KEYS; }; -export const updateOpenAIKey = async (token: string = '', key: string) => { +export const updateOpenAIKeys = async (token: string = '', keys: string[]) => { let error = null; - const res = await fetch(`${OPENAI_API_BASE_URL}/key/update`, { + const res = await fetch(`${OPENAI_API_BASE_URL}/keys/update`, { method: 'POST', headers: { Accept: 'application/json', @@ -110,7 +110,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => { ...(token && { authorization: `Bearer ${token}` }) }, body: JSON.stringify({ - key: key + keys: keys }) }) .then(async (res) => { @@ -131,7 +131,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => { throw error; } - return res.OPENAI_API_KEY; + return res.OPENAI_API_KEYS; }; export const getOpenAIModels = async (token: string = '') => { diff --git a/src/lib/components/chat/Settings/Connections.svelte b/src/lib/components/chat/Settings/Connections.svelte index ed9ddf4de..2f0b821db 100644 --- a/src/lib/components/chat/Settings/Connections.svelte +++ b/src/lib/components/chat/Settings/Connections.svelte @@ -4,7 +4,12 @@ const dispatch = createEventDispatcher(); import { getOllamaUrls, getOllamaVersion, updateOllamaUrls } from '$lib/apis/ollama'; - import { getOpenAIKey, getOpenAIUrl, updateOpenAIKey, updateOpenAIUrl } from '$lib/apis/openai'; + import { + getOpenAIKeys, + getOpenAIUrls, + updateOpenAIKeys, + updateOpenAIUrls + } from '$lib/apis/openai'; import { toast } from 'svelte-sonner'; export let getModels: Function; @@ -16,12 +21,14 @@ let OPENAI_API_KEY = ''; let OPENAI_API_BASE_URL = ''; + let OPENAI_API_KEYS = ['']; + let OPENAI_API_BASE_URLS = ['']; + let showOpenAI = false; - let showLiteLLM = false; const updateOpenAIHandler = async () => { - OPENAI_API_BASE_URL = await updateOpenAIUrl(localStorage.token, OPENAI_API_BASE_URL); - OPENAI_API_KEY = await updateOpenAIKey(localStorage.token, OPENAI_API_KEY); + OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS); + OPENAI_API_KEYS = await updateOpenAIKeys(localStorage.token, OPENAI_API_KEYS); await models.set(await getModels()); }; @@ -43,8 +50,8 @@ onMount(async () => { if ($user.role === 'admin') { OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token); - OPENAI_API_BASE_URL = await getOpenAIUrl(localStorage.token); - OPENAI_API_KEY = await getOpenAIKey(localStorage.token); + OPENAI_API_BASE_URLS = await getOpenAIUrls(localStorage.token); + OPENAI_API_KEYS = await getOpenAIKeys(localStorage.token); } }); @@ -71,37 +78,74 @@ {#if showOpenAI} -
-
API Key
-
-
- -
-
-
+
+ {#each OPENAI_API_BASE_URLS as url, idx} +
+
+ +
-
-
API Base URL
-
-
- +
+ +
+
+ {#if idx === 0} + + {:else} + + {/if} +
-
-
- WebUI will make requests to '{OPENAI_API_BASE_URL}/chat' -
+
+ WebUI will make requests to '{url}/models' +
+ {/each}
{/if}
diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index dd96095b1..99edab7ab 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -97,14 +97,11 @@ if (localDBChats.length === 0) { await deleteDB('Chats'); } - - console.log('localdb', localDBChats); } console.log(DB); } catch (error) { // IndexedDB Not Found - console.log('IDB Not Found'); } console.log(); From bb98c10abbc1a5d25712937c00a743bc8a8cb016 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Wed, 6 Mar 2024 17:04:40 -0800 Subject: [PATCH 21/30] revert: ocr feature --- backend/apps/rag/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py index 45ad69707..99aa69594 100644 --- a/backend/apps/rag/main.py +++ b/backend/apps/rag/main.py @@ -425,7 +425,7 @@ def get_loader(filename: str, file_content_type: str, file_path: str): ] if file_ext == "pdf": - loader = PyPDFLoader(file_path, extract_images=True) + loader = PyPDFLoader(file_path) elif file_ext == "csv": loader = CSVLoader(file_path) elif file_ext == "rst": From b88c64f80eea89cbd698ce20e2df6b22ecab8b33 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Wed, 6 Mar 2024 17:54:42 -0800 Subject: [PATCH 22/30] fix: ocr issue --- backend/apps/rag/main.py | 2 +- backend/requirements.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py index 99aa69594..45ad69707 100644 --- a/backend/apps/rag/main.py +++ b/backend/apps/rag/main.py @@ -425,7 +425,7 @@ def get_loader(filename: str, file_content_type: str, file_path: str): ] if file_ext == "pdf": - loader = PyPDFLoader(file_path) + loader = PyPDFLoader(file_path, extract_images=True) elif file_ext == "csv": loader = CSVLoader(file_path) elif file_ext == "rst": diff --git a/backend/requirements.txt b/backend/requirements.txt index 190d12980..fd38df0b0 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -35,6 +35,7 @@ openpyxl pyxlsb xlrd rapidocr-onnxruntime +opencv-python-headless faster-whisper From 53adc6a0cad17a9a9b94a54585283cb61ae7eaff Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Wed, 6 Mar 2024 18:37:40 -0800 Subject: [PATCH 23/30] fix: rag issue --- backend/apps/ollama/main.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/backend/apps/ollama/main.py b/backend/apps/ollama/main.py index f4442d04f..f8f166d01 100644 --- a/backend/apps/ollama/main.py +++ b/backend/apps/ollama/main.py @@ -222,7 +222,7 @@ async def pull_model( r = requests.request( method="POST", url=f"{url}/api/pull", - data=form_data.model_dump_json(exclude_none=True), + data=form_data.model_dump_json(exclude_none=True).encode(), stream=True, ) @@ -294,7 +294,7 @@ async def push_model( r = requests.request( method="POST", url=f"{url}/api/push", - data=form_data.model_dump_json(exclude_none=True), + data=form_data.model_dump_json(exclude_none=True).encode(), ) r.raise_for_status() @@ -356,7 +356,7 @@ async def create_model( r = requests.request( method="POST", url=f"{url}/api/create", - data=form_data.model_dump_json(exclude_none=True), + data=form_data.model_dump_json(exclude_none=True).encode(), stream=True, ) @@ -419,7 +419,7 @@ async def copy_model( r = requests.request( method="POST", url=f"{url}/api/copy", - data=form_data.model_dump_json(exclude_none=True), + data=form_data.model_dump_json(exclude_none=True).encode(), ) r.raise_for_status() @@ -466,7 +466,7 @@ async def delete_model( r = requests.request( method="DELETE", url=f"{url}/api/delete", - data=form_data.model_dump_json(exclude_none=True), + data=form_data.model_dump_json(exclude_none=True).encode(), ) r.raise_for_status() @@ -506,7 +506,7 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_current_use r = requests.request( method="POST", url=f"{url}/api/show", - data=form_data.model_dump_json(exclude_none=True), + data=form_data.model_dump_json(exclude_none=True).encode(), ) r.raise_for_status() @@ -558,7 +558,7 @@ async def generate_embeddings( r = requests.request( method="POST", url=f"{url}/api/embeddings", - data=form_data.model_dump_json(exclude_none=True), + data=form_data.model_dump_json(exclude_none=True).encode(), ) r.raise_for_status() @@ -644,7 +644,7 @@ async def generate_completion( r = requests.request( method="POST", url=f"{url}/api/generate", - data=form_data.model_dump_json(exclude_none=True), + data=form_data.model_dump_json(exclude_none=True).encode(), stream=True, ) @@ -714,7 +714,7 @@ async def generate_chat_completion( r = None - print(form_data.model_dump_json(exclude_none=True)) + print(form_data.model_dump_json(exclude_none=True).encode()) def get_request(): nonlocal form_data @@ -744,7 +744,7 @@ async def generate_chat_completion( r = requests.request( method="POST", url=f"{url}/api/chat", - data=form_data.model_dump_json(exclude_none=True), + data=form_data.model_dump_json(exclude_none=True).encode(), stream=True, ) @@ -756,6 +756,7 @@ async def generate_chat_completion( headers=dict(r.headers), ) except Exception as e: + print(e) raise e try: @@ -843,7 +844,7 @@ async def generate_openai_chat_completion( r = requests.request( method="POST", url=f"{url}/v1/chat/completions", - data=form_data.model_dump_json(exclude_none=True), + data=form_data.model_dump_json(exclude_none=True).encode(), stream=True, ) From 0476c5f30b798000745d6a56f3d27c492a812230 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Wed, 6 Mar 2024 18:39:33 -0800 Subject: [PATCH 24/30] fix: add doc modal issue --- src/lib/components/documents/AddDocModal.svelte | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/components/documents/AddDocModal.svelte b/src/lib/components/documents/AddDocModal.svelte index 549d76593..959f9d57d 100644 --- a/src/lib/components/documents/AddDocModal.svelte +++ b/src/lib/components/documents/AddDocModal.svelte @@ -138,7 +138,9 @@