Browse Source

Merge branch 'ollama-webui:main' into main

Marclass 1 year ago
parent
commit
8dacc86ab0

+ 22 - 18
backend/apps/web/routers/chats.py

@@ -17,7 +17,8 @@ from apps.web.models.chats import (
 )
 
 from utils.utils import (
-    bearer_scheme, )
+    bearer_scheme,
+)
 from constants import ERROR_MESSAGES
 
 router = APIRouter()
@@ -29,7 +30,8 @@ router = APIRouter()
 
 @router.get("/", response_model=List[ChatTitleIdResponse])
 async def get_user_chats(
-        user=Depends(get_current_user), skip: int = 0, limit: int = 50):
+    user=Depends(get_current_user), skip: int = 0, limit: int = 50
+):
     return Chats.get_chat_lists_by_user_id(user.id, skip, limit)
 
 
@@ -41,9 +43,8 @@ async def get_user_chats(
 @router.get("/all", response_model=List[ChatResponse])
 async def get_all_user_chats(user=Depends(get_current_user)):
     return [
-        ChatResponse(**{
-            **chat.model_dump(), "chat": json.loads(chat.chat)
-        }) for chat in Chats.get_all_chats_by_user_id(user.id)
+        ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
+        for chat in Chats.get_all_chats_by_user_id(user.id)
     ]
 
 
@@ -54,8 +55,14 @@ async def get_all_user_chats(user=Depends(get_current_user)):
 
 @router.post("/new", response_model=Optional[ChatResponse])
 async def create_new_chat(form_data: ChatForm, user=Depends(get_current_user)):
-    chat = Chats.insert_new_chat(user.id, form_data)
-    return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
+    try:
+        chat = Chats.insert_new_chat(user.id, form_data)
+        return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
+    except Exception as e:
+        print(e)
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
+        )
 
 
 ############################
@@ -68,12 +75,11 @@ async def get_chat_by_id(id: str, user=Depends(get_current_user)):
     chat = Chats.get_chat_by_id_and_user_id(id, user.id)
 
     if chat:
-        return ChatResponse(**{
-            **chat.model_dump(), "chat": json.loads(chat.chat)
-        })
+        return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
     else:
-        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
-                            detail=ERROR_MESSAGES.NOT_FOUND)
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
+        )
 
 
 ############################
@@ -82,17 +88,15 @@ async def get_chat_by_id(id: str, user=Depends(get_current_user)):
 
 
 @router.post("/{id}", response_model=Optional[ChatResponse])
-async def update_chat_by_id(id: str,
-                            form_data: ChatForm,
-                            user=Depends(get_current_user)):
+async def update_chat_by_id(
+    id: str, form_data: ChatForm, user=Depends(get_current_user)
+):
     chat = Chats.get_chat_by_id_and_user_id(id, user.id)
     if chat:
         updated_chat = {**json.loads(chat.chat), **form_data.chat}
 
         chat = Chats.update_chat_by_id(id, updated_chat)
-        return ChatResponse(**{
-            **chat.model_dump(), "chat": json.loads(chat.chat)
-        })
+        return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
     else:
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,

+ 4 - 1
backend/start.sh

@@ -1,4 +1,7 @@
 #!/usr/bin/env bash
 
+SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+cd "$SCRIPT_DIR" || exit
+
 PORT="${PORT:-8080}"
-uvicorn main:app --host 0.0.0.0 --port $PORT --forwarded-allow-ips '*'
+uvicorn main:app --host 0.0.0.0 --port "$PORT" --forwarded-allow-ips '*'

+ 204 - 0
docs/apache.md

@@ -0,0 +1,204 @@
+# Hosting UI and Models separately
+
+Sometimes, its beneficial to host Ollama, separate from the UI, but retain the RAG and RBAC support features shared across users:
+
+# Ollama WebUI Configuration
+
+## UI Configuration
+
+For the UI configuration, you can set up the Apache VirtualHost as follows:
+
+```
+# Assuming you have a website hosting this UI at "server.com"
+<VirtualHost 192.168.1.100:80>
+    ServerName server.com
+    DocumentRoot /home/server/public_html
+
+    ProxyPass / http://server.com:3000/ nocanon
+    ProxyPassReverse / http://server.com:3000/
+
+</VirtualHost>
+```
+
+Enable the site first before you can request SSL:
+
+`a2ensite server.com.conf` # this will enable the site. a2ensite is short for "Apache 2 Enable Site"
+
+
+```
+# For SSL
+<VirtualHost 192.168.1.100:443>
+    ServerName server.com
+    DocumentRoot /home/server/public_html
+
+    ProxyPass / http://server.com:3000/ nocanon
+    ProxyPassReverse / http://server.com:3000/
+
+    SSLEngine on
+    SSLCertificateFile /etc/ssl/virtualmin/170514456861234/ssl.cert
+    SSLCertificateKeyFile /etc/ssl/virtualmin/170514456861234/ssl.key
+    SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
+
+    SSLProxyEngine on
+    SSLCACertificateFile /etc/ssl/virtualmin/170514456865864/ssl.ca
+</VirtualHost>
+
+```
+
+I'm using virtualmin here for my SSL clusters, but you can also use certbot directly or your preferred SSL method. To use SSL:
+
+### Prerequisites.
+
+Run the following commands:
+
+`snap install certbot --classic`
+`snap apt install python3-certbot-apache` (this will install the apache plugin).
+
+Navigate to the apache sites-available directory:
+
+`cd /etc/apache2/sites-available/`
+
+Create server.com.conf if it is not yet already created, containing the above `<virtualhost>` configuration (it should match your case. Modify as necessary). Use the one without the SSL:
+
+Once it's created, run `certbot --apache -d server.com`, this will request and add/create an SSL keys for you as well as create the server.com.le-ssl.conf
+
+
+# Configuring Ollama Server
+
+On your latest installation of Ollama, make sure that you have setup your api server from the official Ollama reference:
+
+[Ollama FAQ](https://github.com/jmorganca/ollama/blob/main/docs/faq.md)
+
+
+### TL;DR
+
+The guide doesn't seem to match the current updated service file on linux. So, we will address it here:
+
+Unless when you're compiling Ollama from source, installing with the standard install `curl https://ollama.ai/install.sh | sh` creates a file called `ollama.service` in /etc/systemd/system. You can use nano to edit the file:
+
+```
+sudo nano /etc/systemd/system/ollama.service
+```
+
+Add the following lines:
+```
+Environment="OLLAMA_HOST=0.0.0.0:11434" # this line is mandatory. You can also specify
+```
+
+For instance:
+
+```
+[Unit]
+Description=Ollama Service
+After=network-online.target
+
+[Service]
+ExecStart=/usr/local/bin/ollama serve
+Environment="OLLAMA_HOST=0.0.0.0:11434" # this line is mandatory. You can also specify 192.168.254.109:DIFFERENT_PORT, format
+Environment="OLLAMA_ORIGINS=http://192.168.254.106:11434,https://models.server.city" # this line is optional
+User=ollama
+Group=ollama
+Restart=always
+RestartSec=3
+Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/s>
+
+[Install]
+WantedBy=default.target
+```
+
+
+Save the file by pressing CTRL+S, then press CTRL+X
+
+When your computer restarts, the Ollama server will now be listening on the IP:PORT you specified, in this case 0.0.0.0:11434, or 192.168.254.106:11434 (whatever your local IP address is). Make sure that your router is correctly configured to serve pages from that local IP by forwarding 11434 to your local IP server.
+
+
+# Ollama Model Configuration
+## For the Ollama model configuration, use the following Apache VirtualHost setup:
+
+
+Navigate to the apache sites-available directory:
+
+`cd /etc/apache2/sites-available/`
+
+`nano models.server.city.conf` # match this with your ollama server domain
+
+Add the folloing virtualhost containing this example (modify as needed):
+
+```
+
+# Assuming you have a website hosting this UI at "models.server.city"
+<IfModule mod_ssl.c>
+    <VirtualHost 192.168.254.109:443>
+        DocumentRoot "/var/www/html/"
+        ServerName models.server.city
+        <Directory "/var/www/html/">
+            Options None
+            Require all granted
+        </Directory>
+
+        ProxyRequests Off
+        ProxyPreserveHost On
+        ProxyAddHeaders On
+        SSLProxyEngine on
+
+        ProxyPass / http://server.city:1000/ nocanon # or port 11434
+        ProxyPassReverse / http://server.city:1000/ # or port 11434
+
+        SSLCertificateFile /etc/letsencrypt/live/models.server.city/fullchain.pem
+        SSLCertificateKeyFile /etc/letsencrypt/live/models.server.city/privkey.pem
+        Include /etc/letsencrypt/options-ssl-apache.conf
+    </VirtualHost>
+</IfModule>
+```
+
+You may need to enable the site first (if you haven't done so yet) before you can request SSL:
+
+`a2ensite models.server.city.conf`
+
+#### For the SSL part of Ollama server
+
+Run the following commands:
+
+Navigate to the apache sites-available directory:
+
+`cd /etc/apache2/sites-available/`
+`certbot --apache -d server.com`
+
+```
+<VirtualHost 192.168.254.109:80>
+    DocumentRoot "/var/www/html/"
+    ServerName models.server.city
+    <Directory "/var/www/html/">
+        Options None
+        Require all granted
+    </Directory>
+
+    ProxyRequests Off
+    ProxyPreserveHost On
+    ProxyAddHeaders On
+    SSLProxyEngine on
+
+    ProxyPass / http://server.city:1000/ nocanon # or port 11434
+    ProxyPassReverse / http://server.city:1000/ # or port 11434
+
+    RewriteEngine on
+    RewriteCond %{SERVER_NAME} =models.server.city
+    RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
+</VirtualHost>
+
+```
+
+Don't forget to restart/reload Apache with `systemctl reload apache2`
+
+Open your site at https://server.com!
+
+**Congratulations**, your _**Open-AI-like Chat-GPT style UI**_ is now serving AI with RAG, RBAC and multimodal features! Download Ollama models if you haven't yet done so!
+
+If you encounter any misconfiguration or errors, please file an issue or engage with our discussion. There are a lot of friendly developers here to assist you.
+
+Let's make this UI much more user friendly for everyone!
+
+Thanks for making ollama-webui your UI Choice for AI!
+
+
+This doc is made by **Bob Reyes**, your **Ollama-Web-UI** fan from the Philippines.

+ 6 - 3
src/lib/components/chat/MessageInput.svelte

@@ -301,7 +301,10 @@
 							const file = inputFiles[0];
 							if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) {
 								reader.readAsDataURL(file);
-							} else if (SUPPORTED_FILE_TYPE.includes(file['type'])) {
+							} else if (
+								SUPPORTED_FILE_TYPE.includes(file['type']) ||
+								['md'].includes(file.name.split('.').at(-1))
+							) {
 								uploadDoc(file);
 								filesInputElement.value = '';
 							} else {
@@ -461,8 +464,8 @@
 							placeholder={chatInputPlaceholder !== ''
 								? chatInputPlaceholder
 								: speechRecognitionListening
-								? 'Listening...'
-								: 'Send a message'}
+									? 'Listening...'
+									: 'Send a message'}
 							bind:value={prompt}
 							on:keypress={(e) => {
 								if (e.keyCode == 13 && !e.shiftKey) {

+ 8 - 1
src/lib/components/chat/SettingsModal.svelte

@@ -21,7 +21,7 @@
 	import { WEB_UI_VERSION, WEBUI_API_BASE_URL } from '$lib/constants';
 
 	import { config, models, settings, user, chats } from '$lib/stores';
-	import { splitStream, getGravatarURL } from '$lib/utils';
+	import { splitStream, getGravatarURL, getImportOrigin, convertOpenAIChats } from '$lib/utils';
 
 	import Advanced from './Settings/Advanced.svelte';
 	import Modal from '../common/Modal.svelte';
@@ -132,6 +132,13 @@
 		reader.onload = (event) => {
 			let chats = JSON.parse(event.target.result);
 			console.log(chats);
+			if (getImportOrigin(chats) == 'openai') {
+				try {
+					chats = convertOpenAIChats(chats);
+				} catch (error) {
+					console.log('Unable to import chats:', error);
+				}
+			}
 			importChats(chats);
 		};
 

+ 71 - 0
src/lib/utils/index.ts

@@ -192,3 +192,74 @@ export const calculateSHA256 = async (file) => {
 		throw error;
 	}
 };
+
+export const getImportOrigin = (_chats) => {
+	// Check what external service chat imports are from
+	if ('mapping' in _chats[0]) {
+		return 'openai';
+	}
+	return 'webui';
+};
+
+const convertOpenAIMessages = (convo) => {
+	// Parse OpenAI chat messages and create chat dictionary for creating new chats
+	const mapping = convo['mapping'];
+	const messages = [];
+	let currentId = '';
+
+	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);
+		}
+	}
+
+	let history = {};
+	messages.forEach((obj) => (history[obj.id] = obj));
+
+	const chat = {
+		history: {
+			currentId: currentId,
+			messages: history // Need to convert this to not a list and instead a json object
+		},
+		models: ['gpt-3.5-turbo'],
+		messages: messages,
+		options: {},
+		timestamp: convo['create_time'],
+		title: convo['title'] ?? 'New Chat'
+	};
+	return chat;
+};
+
+export const convertOpenAIChats = (_chats) => {
+	// Create a list of dictionaries with each conversation from import
+	const chats = [];
+	for (let convo of _chats) {
+		const chat = convertOpenAIMessages(convo);
+
+		if (Object.keys(chat.history.messages).length > 0) {
+			chats.push({
+				id: convo['id'],
+				user_id: '',
+				title: convo['title'],
+				chat: chat,
+				timestamp: convo['timestamp']
+			});
+		}
+	}
+	return chats;
+};

+ 9 - 2
src/routes/(app)/c/[id]/+page.svelte

@@ -200,8 +200,15 @@
 					await chatId.set('local');
 				}
 				await tick();
-			}
+			} else if (chat.chat["models"] != selectedModels) {
+				// If model is not saved in DB, then save selectedmodel when message is sent
 
+				chat = await updateChatById(localStorage.token, $chatId, {
+						models: selectedModels
+					});
+				await chats.set(await getChatList(localStorage.token));
+			}
+			
 			// Reset chat input textarea
 			prompt = '';
 			files = [];
@@ -696,7 +703,7 @@
 	<div class="min-h-screen w-full flex justify-center">
 		<div class=" py-2.5 flex flex-col justify-between w-full">
 			<div class="max-w-2xl mx-auto w-full px-3 md:px-0 mt-10">
-				<ModelSelector bind:selectedModels disabled={messages.length > 0} />
+				<ModelSelector bind:selectedModels disabled={messages.length > 0 && !selectedModels.includes('')} />
 			</div>
 
 			<div class=" h-full mt-10 mb-32 w-full flex flex-col">

+ 8 - 2
src/routes/(app)/documents/+page.svelte

@@ -67,7 +67,10 @@
 
 			if (inputFiles && inputFiles.length > 0) {
 				const file = inputFiles[0];
-				if (SUPPORTED_FILE_TYPE.includes(file['type'])) {
+				if (
+					SUPPORTED_FILE_TYPE.includes(file['type']) ||
+					['md'].includes(file.name.split('.').at(-1))
+				) {
 					uploadDoc(file);
 				} else {
 					toast.error(`Unsupported File Type '${file['type']}'.`);
@@ -144,7 +147,10 @@
 				on:change={async (e) => {
 					if (inputFiles && inputFiles.length > 0) {
 						const file = inputFiles[0];
-						if (SUPPORTED_FILE_TYPE.includes(file['type'])) {
+						if (
+							SUPPORTED_FILE_TYPE.includes(file['type']) ||
+							['md'].includes(file.name.split('.').at(-1))
+						) {
 							uploadDoc(file);
 						} else {
 							toast.error(`Unsupported File Type '${file['type']}'.`);