From 91743310255c7aac0db37ccb4790909582d7f6a9 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Mon, 25 Dec 2023 21:44:28 -0800 Subject: [PATCH 01/43] feat: db migration to sqlite --- backend/.gitignore | 5 +- backend/apps/ollama/main.py | 2 +- backend/apps/web/internal/db.py | 4 ++ backend/apps/web/main.py | 3 +- backend/apps/web/models/auths.py | 36 +++++++--- backend/apps/web/models/chats.py | 108 ++++++++++++++++++++++++++++++ backend/apps/web/models/users.py | 68 ++++++++++++------- backend/apps/web/routers/auths.py | 4 +- backend/apps/web/routers/chats.py | 100 +++++++++++++++++++++++++++ backend/config.py | 20 +----- backend/constants.py | 6 ++ backend/requirements.txt | 4 +- src/routes/auth/+page.svelte | 2 +- 13 files changed, 302 insertions(+), 60 deletions(-) create mode 100644 backend/apps/web/internal/db.py create mode 100644 backend/apps/web/models/chats.py create mode 100644 backend/apps/web/routers/chats.py diff --git a/backend/.gitignore b/backend/.gitignore index bbb8ba18..11f9256f 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,4 +1,7 @@ __pycache__ .env _old -uploads \ No newline at end of file +uploads +.ipynb_checkpoints +*.db +_test \ No newline at end of file diff --git a/backend/apps/ollama/main.py b/backend/apps/ollama/main.py index a2a09fc7..c7961ea7 100644 --- a/backend/apps/ollama/main.py +++ b/backend/apps/ollama/main.py @@ -25,7 +25,7 @@ TARGET_SERVER_URL = OLLAMA_API_BASE_URL def proxy(path): # Combine the base URL of the target server with the requested path target_url = f"{TARGET_SERVER_URL}/{path}" - print(path) + print(target_url) # Get data from the original request data = request.get_data() diff --git a/backend/apps/web/internal/db.py b/backend/apps/web/internal/db.py new file mode 100644 index 00000000..d2f7db95 --- /dev/null +++ b/backend/apps/web/internal/db.py @@ -0,0 +1,4 @@ +from peewee import * + +DB = SqliteDatabase("./ollama.db") +DB.connect() diff --git a/backend/apps/web/main.py b/backend/apps/web/main.py index 854f1626..52238aae 100644 --- a/backend/apps/web/main.py +++ b/backend/apps/web/main.py @@ -1,7 +1,7 @@ from fastapi import FastAPI, Request, Depends, HTTPException from fastapi.middleware.cors import CORSMiddleware -from apps.web.routers import auths, users, utils +from apps.web.routers import auths, users, chats, utils from config import WEBUI_VERSION, WEBUI_AUTH app = FastAPI() @@ -20,6 +20,7 @@ app.add_middleware( app.include_router(auths.router, prefix="/auths", tags=["auths"]) app.include_router(users.router, prefix="/users", tags=["users"]) app.include_router(utils.router, prefix="/utils", tags=["utils"]) +app.include_router(chats.router, prefix="/chats", tags=["chats"]) @app.get("/") diff --git a/backend/apps/web/models/auths.py b/backend/apps/web/models/auths.py index 41c82efd..aef80e2f 100644 --- a/backend/apps/web/models/auths.py +++ b/backend/apps/web/models/auths.py @@ -2,6 +2,7 @@ from pydantic import BaseModel from typing import List, Union, Optional import time import uuid +from peewee import * from apps.web.models.users import UserModel, Users @@ -12,15 +13,23 @@ from utils.utils import ( create_token, ) -import config - -DB = config.DB +from apps.web.internal.db import DB #################### # DB MODEL #################### +class Auth(Model): + id = CharField(unique=True) + email = CharField() + password = CharField() + active = BooleanField() + + class Meta: + database = DB + + class AuthModel(BaseModel): id: str email: str @@ -64,7 +73,7 @@ class SignupForm(BaseModel): class AuthsTable: def __init__(self, db): self.db = db - self.table = db.auths + self.db.create_tables([Auth]) def insert_new_auth( self, email: str, password: str, name: str, role: str = "pending" @@ -76,7 +85,9 @@ class AuthsTable: auth = AuthModel( **{"id": id, "email": email, "password": password, "active": True} ) - result = self.table.insert_one(auth.model_dump()) + result = Auth.create(**auth.model_dump()) + print(result) + user = Users.insert_new_user(id, name, email, role) print(result, user) @@ -86,14 +97,19 @@ class AuthsTable: return None def authenticate_user(self, email: str, password: str) -> Optional[UserModel]: - print("authenticate_user") + print("authenticate_user", email) - auth = self.table.find_one({"email": email, "active": True}) + auth = Auth.get(Auth.email == email, Auth.active == True) + print(auth.email) if auth: - if verify_password(password, auth["password"]): - user = self.db.users.find_one({"id": auth["id"]}) - return UserModel(**user) + print(password, str(auth.password)) + print(verify_password(password, str(auth.password))) + if verify_password(password, auth.password): + user = Users.get_user_by_id(auth.id) + + print(user) + return user else: return None else: diff --git a/backend/apps/web/models/chats.py b/backend/apps/web/models/chats.py new file mode 100644 index 00000000..1105452f --- /dev/null +++ b/backend/apps/web/models/chats.py @@ -0,0 +1,108 @@ +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 + + +#################### +# Chat DB Schema +#################### + + +class Chat(Model): + id = CharField(unique=True) + user_id: CharField() + title = CharField() + chat = TextField() # Save Chat JSON as Text + timestamp = DateField() + + class Meta: + database = DB + + +class ChatModel(BaseModel): + id: str + user_id: str + title: str + chat: dict + timestamp: int # timestamp in epoch + + +#################### +# Forms +#################### + + +class ChatForm(BaseModel): + chat: dict + + +class ChatUpdateForm(ChatForm): + id: str + + +class ChatTitleIdResponse(BaseModel): + id: str + title: str + + +class ChatTable: + def __init__(self, db): + self.db = db + db.create_tables([Chat]) + + def insert_new_chat(self, user_id: str, form_data: ChatForm) -> Optional[ChatModel]: + id = str(uuid.uuid4()) + chat = ChatModel( + **{ + "id": id, + "user_id": user_id, + "title": form_data.chat["title"], + "chat": json.dump(form_data.chat), + "timestamp": int(time.time()), + } + ) + + result = Chat.create(**chat.model_dump()) + return chat if result else None + + def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]: + try: + query = Chat.update(chat=json.dump(chat)).where(Chat.id == id) + query.execute() + + chat = Chat.get(Chat.id == id) + return ChatModel(**model_to_dict(chat)) + except: + return None + + def get_chat_lists_by_user_id( + self, user_id: str, skip: int = 0, limit: int = 50 + ) -> List[ChatModel]: + return [ + ChatModel(**model_to_dict(chat)) + for chat in Chat.select(Chat.user_id == user_id).limit(limit).offset(skip) + ] + + def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]: + try: + chat = Chat.get(Chat.id == id, Chat.user_id == user_id) + return ChatModel(**model_to_dict(chat)) + except: + return None + + def get_chats(self, skip: int = 0, limit: int = 50) -> List[ChatModel]: + return [ + ChatModel(**model_to_dict(chat)) + for chat in Chat.select().limit(limit).offset(skip) + ] + + +Chats = ChatTable(DB) diff --git a/backend/apps/web/models/users.py b/backend/apps/web/models/users.py index 4dc3fc7a..88414999 100644 --- a/backend/apps/web/models/users.py +++ b/backend/apps/web/models/users.py @@ -1,25 +1,41 @@ from pydantic import BaseModel +from peewee import * +from playhouse.shortcuts import model_to_dict from typing import List, Union, Optional -from pymongo import ReturnDocument import time from utils.utils import decode_token from utils.misc import get_gravatar_url -from config import DB +from apps.web.internal.db import DB #################### # User DB Schema #################### +class User(Model): + id = CharField(unique=True) + name = CharField() + email = CharField() + role = CharField() + profile_image_url = CharField() + timestamp = DateField() + + class Meta: + database = DB + + class UserModel(BaseModel): + class Config: + orm_mode = True + id: str name: str email: str role: str = "pending" profile_image_url: str = "/user.png" - created_at: int # timestamp in epoch + timestamp: int # timestamp in epoch #################### @@ -35,7 +51,7 @@ class UserRoleUpdateForm(BaseModel): class UsersTable: def __init__(self, db): self.db = db - self.table = db.users + self.db.create_tables([User]) def insert_new_user( self, id: str, name: str, email: str, role: str = "pending" @@ -47,22 +63,27 @@ class UsersTable: "email": email, "role": role, "profile_image_url": get_gravatar_url(email), - "created_at": int(time.time()), + "timestamp": int(time.time()), } ) - result = self.table.insert_one(user.model_dump()) - + result = User.create(**user.model_dump()) if result: return user else: return None - def get_user_by_email(self, email: str) -> Optional[UserModel]: - user = self.table.find_one({"email": email}, {"_id": False}) + def get_user_by_id(self, id: str) -> Optional[UserModel]: + try: + user = User.get(User.id == id) + return UserModel(**model_to_dict(user)) + except: + return None - if user: - return UserModel(**user) - else: + def get_user_by_email(self, email: str) -> Optional[UserModel]: + try: + user = User.get(User.email == email) + return UserModel(**model_to_dict(user)) + except: return None def get_user_by_token(self, token: str) -> Optional[UserModel]: @@ -75,23 +96,22 @@ class UsersTable: def get_users(self, skip: int = 0, limit: int = 50) -> List[UserModel]: return [ - UserModel(**user) - for user in list( - self.table.find({}, {"_id": False}).skip(skip).limit(limit) - ) + UserModel(**model_to_dict(user)) + for user in User.select().limit(limit).offset(skip) ] def get_num_users(self) -> Optional[int]: - return self.table.count_documents({}) - - def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]: - user = self.table.find_one_and_update( - {"id": id}, {"$set": updated}, return_document=ReturnDocument.AFTER - ) - return UserModel(**user) + return User.select().count() def update_user_role_by_id(self, id: str, role: str) -> Optional[UserModel]: - return self.update_user_by_id(id, {"role": role}) + try: + query = User.update(role=role).where(User.id == id) + query.execute() + + user = User.get(User.id == id) + return UserModel(**model_to_dict(user)) + except: + return None Users = UsersTable(DB) diff --git a/backend/apps/web/routers/auths.py b/backend/apps/web/routers/auths.py index 023e1914..0fb34d47 100644 --- a/backend/apps/web/routers/auths.py +++ b/backend/apps/web/routers/auths.py @@ -104,8 +104,8 @@ async def signup(form_data: SignupForm): "profile_image_url": user.profile_image_url, } else: - raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err)) + raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT()) except Exception as err: raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err)) else: - raise HTTPException(400, detail=ERROR_MESSAGES.DEFAULT()) + raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) diff --git a/backend/apps/web/routers/chats.py b/backend/apps/web/routers/chats.py new file mode 100644 index 00000000..a29f2da5 --- /dev/null +++ b/backend/apps/web/routers/chats.py @@ -0,0 +1,100 @@ +from fastapi import Response +from fastapi import Depends, FastAPI, HTTPException, status +from datetime import datetime, timedelta +from typing import List, Union, Optional + +from fastapi import APIRouter +from pydantic import BaseModel + +from apps.web.models.users import Users +from apps.web.models.chats import ( + ChatModel, + ChatForm, + ChatUpdateForm, + ChatTitleIdResponse, + Chats, +) + +from utils.utils import ( + bearer_scheme, +) +from constants import ERROR_MESSAGES + +router = APIRouter() + +############################ +# GetChats +############################ + + +@router.get("/", response_model=List[ChatTitleIdResponse]) +async def get_user_chats(skip: int = 0, limit: int = 50, cred=Depends(bearer_scheme)): + token = cred.credentials + user = Users.get_user_by_token(token) + + if user: + return Chats.get_chat_titles_and_ids_by_user_id(user.id, skip, limit) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.INVALID_TOKEN, + ) + + +############################ +# CreateNewChat +############################ + + +@router.post("/new", response_model=Optional[ChatModel]) +async def create_new_chat(form_data: ChatForm, cred=Depends(bearer_scheme)): + token = cred.credentials + user = Users.get_user_by_token(token) + + if user: + return Chats.insert_new_chat(user.id, form_data) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.INVALID_TOKEN, + ) + + +############################ +# GetChatById +############################ + + +@router.get("/{id}", response_model=Optional[ChatModel]) +async def get_chat_by_id(id: str, cred=Depends(bearer_scheme)): + token = cred.credentials + user = Users.get_user_by_token(token) + + if user: + return Chats.get_chat_by_id_and_user_id(id, user.id) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.INVALID_TOKEN, + ) + + +############################ +# UpdateChatById +############################ + + +@router.post("/{id}", response_model=Optional[ChatModel]) +async def update_chat_by_id( + id: str, form_data: ChatUpdateForm, cred=Depends(bearer_scheme) +): + token = cred.credentials + user = Users.get_user_by_token(token) + + if user: + return Chats.update_chat_by_id_and_user_id(id, user.id, form_data.chat) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.INVALID_TOKEN, + ) diff --git a/backend/config.py b/backend/config.py index cf9eae02..e0014bd8 100644 --- a/backend/config.py +++ b/backend/config.py @@ -1,9 +1,10 @@ from dotenv import load_dotenv, find_dotenv -from pymongo import MongoClient + from constants import ERROR_MESSAGES from secrets import token_bytes from base64 import b64encode + import os load_dotenv(find_dotenv("../.env")) @@ -36,25 +37,8 @@ WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.40") # WEBUI_AUTH #################################### - WEBUI_AUTH = True if os.environ.get("WEBUI_AUTH", "FALSE") == "TRUE" else False - -#################################### -# WEBUI_DB (Deprecated, Should be removed) -#################################### - - -WEBUI_DB_URL = os.environ.get("WEBUI_DB_URL", "mongodb://root:root@localhost:27017/") - -if WEBUI_AUTH and WEBUI_DB_URL == "": - raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND) - - -DB_CLIENT = MongoClient(f"{WEBUI_DB_URL}?authSource=admin") -DB = DB_CLIENT["ollama-webui"] - - #################################### # WEBUI_JWT_SECRET_KEY #################################### diff --git a/backend/constants.py b/backend/constants.py index b383957b..8301ea0b 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -11,6 +11,12 @@ class ERROR_MESSAGES(str, Enum): DEFAULT = lambda err="": f"Something went wrong :/\n{err if err else ''}" ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now." + + EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew." + USERNAME_TAKEN = ( + "Uh-oh! This username is already registered. Please choose another username." + ) + INVALID_TOKEN = ( "Your session has expired or the token is invalid. Please sign in again." ) diff --git a/backend/requirements.txt b/backend/requirements.txt index 1568f7bb..2644d559 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -13,8 +13,8 @@ uuid requests aiohttp -pymongo +peewee bcrypt PyJWT -pyjwt[crypto] \ No newline at end of file +pyjwt[crypto] diff --git a/src/routes/auth/+page.svelte b/src/routes/auth/+page.svelte index a3d33f2f..9ec0b16f 100644 --- a/src/routes/auth/+page.svelte +++ b/src/routes/auth/+page.svelte @@ -66,7 +66,7 @@ if (res) { console.log(res); - toast.success(`Account creation successful."`); + toast.success(`Account creation successful.`); localStorage.token = res.token; await user.set(res); goto('/'); From 540b50e176e1a7f0138f6dde271df2db56f62a93 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Mon, 25 Dec 2023 21:55:29 -0800 Subject: [PATCH 02/43] feat: wip chat route --- backend/apps/web/models/chats.py | 5 ++++- backend/apps/web/routers/chats.py | 11 +++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/backend/apps/web/models/chats.py b/backend/apps/web/models/chats.py index 1105452f..30a21a80 100644 --- a/backend/apps/web/models/chats.py +++ b/backend/apps/web/models/chats.py @@ -88,7 +88,10 @@ class ChatTable: ) -> List[ChatModel]: return [ ChatModel(**model_to_dict(chat)) - for chat in Chat.select(Chat.user_id == user_id).limit(limit).offset(skip) + for chat in Chat.select(Chat.id, Chat.title) + .where(Chat.user_id == user_id) + .limit(limit) + .offset(skip) ] def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]: diff --git a/backend/apps/web/routers/chats.py b/backend/apps/web/routers/chats.py index a29f2da5..64cee04e 100644 --- a/backend/apps/web/routers/chats.py +++ b/backend/apps/web/routers/chats.py @@ -33,7 +33,7 @@ async def get_user_chats(skip: int = 0, limit: int = 50, cred=Depends(bearer_sch user = Users.get_user_by_token(token) if user: - return Chats.get_chat_titles_and_ids_by_user_id(user.id, skip, limit) + return Chats.get_chat_lists_by_user_id(user.id, skip, limit) else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -92,7 +92,14 @@ async def update_chat_by_id( user = Users.get_user_by_token(token) if user: - return Chats.update_chat_by_id_and_user_id(id, user.id, form_data.chat) + chat = Chats.get_chat_by_id_and_user_id(id, user.id) + if chat: + return Chats.update_chat_by_id(id, form_data.chat) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, From 8d5c3ee56ff68dbec81e6a8d0b2e5bb3a2505553 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Mon, 25 Dec 2023 22:14:06 -0800 Subject: [PATCH 03/43] feat: backend required error page --- backend/config.py | 6 ++--- src/routes/+layout.svelte | 25 ++++++++++++-------- src/routes/error/+page.svelte | 44 +++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 13 deletions(-) create mode 100644 src/routes/error/+page.svelte diff --git a/backend/config.py b/backend/config.py index e0014bd8..8e100fe5 100644 --- a/backend/config.py +++ b/backend/config.py @@ -31,13 +31,13 @@ if ENV == "prod": # WEBUI_VERSION #################################### -WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.40") +WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.42") #################################### -# WEBUI_AUTH +# WEBUI_AUTH (Required for security) #################################### -WEBUI_AUTH = True if os.environ.get("WEBUI_AUTH", "FALSE") == "TRUE" else False +WEBUI_AUTH = True #################################### # WEBUI_JWT_SECRET_KEY diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 7479f559..e19d5f37 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -11,7 +11,8 @@ let loaded = false; onMount(async () => { - const resBackend = await fetch(`${WEBUI_API_BASE_URL}/`, { + // Check Backend Status + const res = await fetch(`${WEBUI_API_BASE_URL}/`, { method: 'GET', headers: { 'Content-Type': 'application/json' @@ -26,13 +27,14 @@ return null; }); - console.log(resBackend); - await config.set(resBackend); + if (res) { + await config.set(res); + console.log(res); - if ($config) { - if ($config.auth) { + if ($config) { if (localStorage.token) { - const res = await fetch(`${WEBUI_API_BASE_URL}/auths`, { + // Get Session User Info + const sessionUser = await fetch(`${WEBUI_API_BASE_URL}/auths`, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -49,8 +51,8 @@ return null; }); - if (res) { - await user.set(res); + if (sessionUser) { + await user.set(sessionUser); } else { localStorage.removeItem('token'); await goto('/auth'); @@ -59,6 +61,8 @@ await goto('/auth'); } } + } else { + await goto(`/error`); } await tick(); @@ -69,8 +73,9 @@ Ollama - -{#if $config !== undefined && loaded} +{#if loaded} {/if} + + diff --git a/src/routes/error/+page.svelte b/src/routes/error/+page.svelte new file mode 100644 index 00000000..5ce3fa03 --- /dev/null +++ b/src/routes/error/+page.svelte @@ -0,0 +1,44 @@ + + +{#if loaded} +
+
+
+
+
Ollama WebUI Backend Required
+ +
+ Oops! It seems like your Ollama WebUI needs a little attention. + describe troubleshooting/installation, help @ discord + + +
+ +
+ +
+
+
+
+
+{/if} From 6350d86bde1e714e28987c3acb3b0458a674032d Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Mon, 25 Dec 2023 23:43:21 -0800 Subject: [PATCH 04/43] fix: chat model schema --- backend/apps/web/models/auths.py | 24 +++++++++++++----------- backend/apps/web/models/chats.py | 14 ++++++++------ 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/backend/apps/web/models/auths.py b/backend/apps/web/models/auths.py index aef80e2f..99a7567e 100644 --- a/backend/apps/web/models/auths.py +++ b/backend/apps/web/models/auths.py @@ -98,21 +98,23 @@ class AuthsTable: def authenticate_user(self, email: str, password: str) -> Optional[UserModel]: print("authenticate_user", email) + try: + auth = Auth.get(Auth.email == email, Auth.active == True) + print(auth.email) - auth = Auth.get(Auth.email == email, Auth.active == True) - print(auth.email) + if auth: + print(password, str(auth.password)) + print(verify_password(password, str(auth.password))) + if verify_password(password, auth.password): + user = Users.get_user_by_id(auth.id) - if auth: - print(password, str(auth.password)) - print(verify_password(password, str(auth.password))) - if verify_password(password, auth.password): - user = Users.get_user_by_id(auth.id) - - print(user) - return user + print(user) + return user + else: + return None else: return None - else: + except: return None diff --git a/backend/apps/web/models/chats.py b/backend/apps/web/models/chats.py index 30a21a80..cd915d9a 100644 --- a/backend/apps/web/models/chats.py +++ b/backend/apps/web/models/chats.py @@ -18,7 +18,7 @@ from apps.web.internal.db import DB class Chat(Model): id = CharField(unique=True) - user_id: CharField() + user_id = CharField() title = CharField() chat = TextField() # Save Chat JSON as Text timestamp = DateField() @@ -31,7 +31,7 @@ class ChatModel(BaseModel): id: str user_id: str title: str - chat: dict + chat: str timestamp: int # timestamp in epoch @@ -64,8 +64,10 @@ class ChatTable: **{ "id": id, "user_id": user_id, - "title": form_data.chat["title"], - "chat": json.dump(form_data.chat), + "title": form_data.chat["title"] + if "title" in form_data.chat + else "New Chat", + "chat": json.dumps(form_data.chat), "timestamp": int(time.time()), } ) @@ -75,7 +77,7 @@ class ChatTable: def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]: try: - query = Chat.update(chat=json.dump(chat)).where(Chat.id == id) + query = Chat.update(chat=json.dumps(chat)).where(Chat.id == id) query.execute() chat = Chat.get(Chat.id == id) @@ -88,7 +90,7 @@ class ChatTable: ) -> List[ChatModel]: return [ ChatModel(**model_to_dict(chat)) - for chat in Chat.select(Chat.id, Chat.title) + for chat in Chat.select() .where(Chat.user_id == user_id) .limit(limit) .offset(skip) From 2cb0bf44390adeefd8c527962369b24f4b0a4184 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Tue, 26 Dec 2023 01:27:43 -0800 Subject: [PATCH 05/43] fix: chat return type to dict --- backend/apps/web/models/chats.py | 22 ++++++++++++++-- backend/apps/web/models/users.py | 3 --- backend/apps/web/routers/chats.py | 42 +++++++++++++++++++++++-------- 3 files changed, 52 insertions(+), 15 deletions(-) diff --git a/backend/apps/web/models/chats.py b/backend/apps/web/models/chats.py index cd915d9a..ad9a2877 100644 --- a/backend/apps/web/models/chats.py +++ b/backend/apps/web/models/chats.py @@ -44,8 +44,12 @@ class ChatForm(BaseModel): chat: dict -class ChatUpdateForm(ChatForm): +class ChatResponse(BaseModel): id: str + user_id: str + title: str + chat: dict + timestamp: int # timestamp in epoch class ChatTitleIdResponse(BaseModel): @@ -77,7 +81,11 @@ class ChatTable: def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]: try: - query = Chat.update(chat=json.dumps(chat)).where(Chat.id == id) + query = Chat.update( + chat=json.dumps(chat), + title=chat["title"] if "title" in chat else "New Chat", + timestamp=int(time.time()), + ).where(Chat.id == id) query.execute() chat = Chat.get(Chat.id == id) @@ -92,6 +100,7 @@ class ChatTable: ChatModel(**model_to_dict(chat)) for chat in Chat.select() .where(Chat.user_id == user_id) + .order_by(Chat.timestamp.desc()) .limit(limit) .offset(skip) ] @@ -109,5 +118,14 @@ class ChatTable: for chat in Chat.select().limit(limit).offset(skip) ] + def delete_chat_by_id_and_user_id(self, id: str, user_id: str) -> bool: + try: + query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id)) + query.execute() # Remove the rows, return number of rows removed. + + return True + except: + return False + Chats = ChatTable(DB) diff --git a/backend/apps/web/models/users.py b/backend/apps/web/models/users.py index 88414999..782b7f47 100644 --- a/backend/apps/web/models/users.py +++ b/backend/apps/web/models/users.py @@ -27,9 +27,6 @@ class User(Model): class UserModel(BaseModel): - class Config: - orm_mode = True - id: str name: str email: str diff --git a/backend/apps/web/routers/chats.py b/backend/apps/web/routers/chats.py index 64cee04e..3191db8b 100644 --- a/backend/apps/web/routers/chats.py +++ b/backend/apps/web/routers/chats.py @@ -5,12 +5,13 @@ from typing import List, Union, Optional from fastapi import APIRouter from pydantic import BaseModel +import json from apps.web.models.users import Users from apps.web.models.chats import ( ChatModel, + ChatResponse, ChatForm, - ChatUpdateForm, ChatTitleIdResponse, Chats, ) @@ -46,13 +47,14 @@ async def get_user_chats(skip: int = 0, limit: int = 50, cred=Depends(bearer_sch ############################ -@router.post("/new", response_model=Optional[ChatModel]) +@router.post("/new", response_model=Optional[ChatResponse]) async def create_new_chat(form_data: ChatForm, cred=Depends(bearer_scheme)): token = cred.credentials user = Users.get_user_by_token(token) if user: - return Chats.insert_new_chat(user.id, form_data) + chat = Chats.insert_new_chat(user.id, form_data) + return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -65,13 +67,14 @@ async def create_new_chat(form_data: ChatForm, cred=Depends(bearer_scheme)): ############################ -@router.get("/{id}", response_model=Optional[ChatModel]) +@router.get("/{id}", response_model=Optional[ChatResponse]) async def get_chat_by_id(id: str, cred=Depends(bearer_scheme)): token = cred.credentials user = Users.get_user_by_token(token) if user: - return Chats.get_chat_by_id_and_user_id(id, user.id) + chat = Chats.get_chat_by_id_and_user_id(id, user.id) + return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -84,17 +87,16 @@ async def get_chat_by_id(id: str, cred=Depends(bearer_scheme)): ############################ -@router.post("/{id}", response_model=Optional[ChatModel]) -async def update_chat_by_id( - id: str, form_data: ChatUpdateForm, cred=Depends(bearer_scheme) -): +@router.post("/{id}", response_model=Optional[ChatResponse]) +async def update_chat_by_id(id: str, form_data: ChatForm, cred=Depends(bearer_scheme)): token = cred.credentials user = Users.get_user_by_token(token) if user: chat = Chats.get_chat_by_id_and_user_id(id, user.id) if chat: - return Chats.update_chat_by_id(id, form_data.chat) + chat = Chats.update_chat_by_id(id, form_data.chat) + return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -105,3 +107,23 @@ async def update_chat_by_id( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.INVALID_TOKEN, ) + + +############################ +# DeleteChatById +############################ + + +@router.delete("/{id}", response_model=bool) +async def delete_chat_by_id(id: str, cred=Depends(bearer_scheme)): + token = cred.credentials + user = Users.get_user_by_token(token) + + if user: + result = Chats.delete_chat_by_id_and_user_id(id, user.id) + return result + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.INVALID_TOKEN, + ) From 1274bd986b7c8705d4f6d6a8c615e62e547cfe3c Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Tue, 26 Dec 2023 02:22:06 -0800 Subject: [PATCH 06/43] chore: dockerignore added --- .dockerignore | 16 ++++++++++++++++ backend/.dockerignore | 7 +++++++ 2 files changed, 23 insertions(+) create mode 100644 .dockerignore create mode 100644 backend/.dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..0221b085 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +.DS_Store +node_modules +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +__pycache__ +.env +_old +uploads +.ipynb_checkpoints +*.db +_test \ No newline at end of file diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 00000000..11f9256f --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,7 @@ +__pycache__ +.env +_old +uploads +.ipynb_checkpoints +*.db +_test \ No newline at end of file From 0810a2648f18649f80102e04c45f26a0f1788efc Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Tue, 26 Dec 2023 03:28:30 -0800 Subject: [PATCH 07/43] feat: multi-user chat history support --- .dockerignore | 2 +- run.sh | 2 +- src/lib/apis/chats/index.ts | 162 ++++++++++++ src/lib/apis/index.ts | 35 +++ src/lib/apis/ollama/index.ts | 71 ++++++ src/lib/components/chat/Messages.svelte | 5 +- src/lib/components/layout/Navbar.svelte | 10 +- src/lib/components/layout/Sidebar.svelte | 21 +- src/routes/(app)/+layout.svelte | 235 ++++++++---------- src/routes/(app)/+page.svelte | 63 ++--- src/routes/(app)/c/[id]/+page.svelte | 83 ++++--- .../(app)/modelfiles/create/+page.svelte | 1 - src/routes/admin/+page.svelte | 4 +- src/routes/auth/+page.svelte | 2 +- src/routes/error/+page.svelte | 15 +- 15 files changed, 495 insertions(+), 216 deletions(-) create mode 100644 src/lib/apis/chats/index.ts create mode 100644 src/lib/apis/index.ts create mode 100644 src/lib/apis/ollama/index.ts diff --git a/.dockerignore b/.dockerignore index 0221b085..419f53fb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,5 +12,5 @@ __pycache__ _old uploads .ipynb_checkpoints -*.db +**/*.db _test \ No newline at end of file diff --git a/run.sh b/run.sh index 584c7f64..e2fae795 100644 --- a/run.sh +++ b/run.sh @@ -1,5 +1,5 @@ docker stop ollama-webui || true docker rm ollama-webui || true docker build -t ollama-webui . -docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway --name ollama-webui --restart always ollama-webui +docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v ollama-webui:/app --name ollama-webui --restart always ollama-webui docker image prune -f \ No newline at end of file diff --git a/src/lib/apis/chats/index.ts b/src/lib/apis/chats/index.ts new file mode 100644 index 00000000..9c421816 --- /dev/null +++ b/src/lib/apis/chats/index.ts @@ -0,0 +1,162 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const createNewChat = async (token: string, chat: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/new`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + chat: chat + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getChatlist = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/`, { + 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 getChatById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}`, { + 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 updateChatById = async (token: string, id: string, chat: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + chat: chat + }) + }) + .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 deleteChatById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}`, { + 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; +}; diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts new file mode 100644 index 00000000..6b6f9631 --- /dev/null +++ b/src/lib/apis/index.ts @@ -0,0 +1,35 @@ +export const getOpenAIModels = async ( + base_url: string = 'https://api.openai.com/v1', + api_key: string = '' +) => { + let error = null; + + const res = await fetch(`${base_url}/models`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${api_key}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((error) => { + console.log(error); + error = `OpenAI: ${error?.error?.message ?? 'Network Problem'}`; + return null; + }); + + if (error) { + throw error; + } + + let models = Array.isArray(res) ? res : res?.data ?? null; + + console.log(models); + + return models + .map((model) => ({ name: model.id, external: true })) + .filter((model) => (base_url.includes('openai') ? model.name.includes('gpt') : true)); +}; diff --git a/src/lib/apis/ollama/index.ts b/src/lib/apis/ollama/index.ts new file mode 100644 index 00000000..67adcdf6 --- /dev/null +++ b/src/lib/apis/ollama/index.ts @@ -0,0 +1,71 @@ +import { OLLAMA_API_BASE_URL } from '$lib/constants'; + +export const getOllamaVersion = async ( + base_url: string = OLLAMA_API_BASE_URL, + token: string = '' +) => { + let error = null; + + const res = await fetch(`${base_url}/version`, { + 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(); + }) + .catch((error) => { + console.log(error); + if ('detail' in error) { + error = error.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res?.version ?? '0'; +}; + +export const getOllamaModels = async ( + base_url: string = OLLAMA_API_BASE_URL, + token: string = '' +) => { + let error = null; + + const res = await fetch(`${base_url}/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(); + }) + .catch((error) => { + console.log(error); + if ('detail' in error) { + error = error.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res?.models ?? []; +}; diff --git a/src/lib/components/chat/Messages.svelte b/src/lib/components/chat/Messages.svelte index 543ce6a5..072ade46 100644 --- a/src/lib/components/chat/Messages.svelte +++ b/src/lib/components/chat/Messages.svelte @@ -8,11 +8,12 @@ import auto_render from 'katex/dist/contrib/auto-render.mjs'; import 'katex/dist/katex.min.css'; - import { chatId, config, db, modelfiles, settings, user } from '$lib/stores'; + import { config, db, modelfiles, settings, user } from '$lib/stores'; import { tick } from 'svelte'; import toast from 'svelte-french-toast'; + export let chatId = ''; export let sendPrompt: Function; export let regenerateResponse: Function; @@ -239,7 +240,7 @@ history.currentId = userMessageId; await tick(); - await sendPrompt(userPrompt, userMessageId, $chatId); + await sendPrompt(userPrompt, userMessageId, chatId); }; const confirmEditResponseMessage = async (messageId) => { diff --git a/src/lib/components/layout/Navbar.svelte b/src/lib/components/layout/Navbar.svelte index bcd66ee9..219801bf 100644 --- a/src/lib/components/layout/Navbar.svelte +++ b/src/lib/components/layout/Navbar.svelte @@ -5,11 +5,12 @@ import { chatId, db, modelfiles } from '$lib/stores'; import toast from 'svelte-french-toast'; + export let initNewChat: Function; export let title: string = 'Ollama Web UI'; export let shareEnabled: boolean = false; const shareChat = async () => { - const chat = await $db.getChatById($chatId); + const chat = (await $db.getChatById($chatId)).chat; console.log('share', chat); toast.success('Redirecting you to OllamaHub'); @@ -44,12 +45,9 @@
+ + +
+
+ + + + {:else if ($info?.ollama?.version ?? '0').localeCompare( requiredOllamaVersion, undefined, { numeric: true, sensitivity: 'case', caseFirst: 'upper' } ) < 0}
- -
diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte index 0d1055f9..e7e8a03c 100644 --- a/src/routes/(app)/+page.svelte +++ b/src/routes/(app)/+page.svelte @@ -2,18 +2,18 @@ import { v4 as uuidv4 } from 'uuid'; import toast from 'svelte-french-toast'; - import { OLLAMA_API_BASE_URL } from '$lib/constants'; - import { onMount, tick } from 'svelte'; - import { splitStream } from '$lib/utils'; + import { onDestroy, onMount, tick } from 'svelte'; import { goto } from '$app/navigation'; + import { page } from '$app/stores'; import { config, models, modelfiles, user, settings, db, chats, chatId } from '$lib/stores'; + import { OLLAMA_API_BASE_URL } from '$lib/constants'; + import { splitStream } from '$lib/utils'; import MessageInput from '$lib/components/chat/MessageInput.svelte'; import Messages from '$lib/components/chat/Messages.svelte'; import ModelSelector from '$lib/components/chat/ModelSelector.svelte'; import Navbar from '$lib/components/layout/Navbar.svelte'; - import { page } from '$app/stores'; let stopResponseFlag = false; let autoScroll = true; @@ -26,10 +26,11 @@ ? $modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0])[0] : null; + let chat = null; + let title = ''; let prompt = ''; let files = []; - let messages = []; let history = { messages: {}, @@ -50,16 +51,8 @@ messages = []; } - $: if (files) { - console.log(files); - } - onMount(async () => { - await chatId.set(uuidv4()); - - chatId.subscribe(async () => { - await initNewChat(); - }); + await initNewChat(); }); ////////////////////////// @@ -67,6 +60,9 @@ ////////////////////////// const initNewChat = async () => { + console.log('initNewChat'); + + await chatId.set(''); console.log($chatId); autoScroll = true; @@ -82,7 +78,6 @@ : $settings.models ?? ['']; let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); - console.log(_settings); settings.set({ ..._settings }); @@ -127,14 +122,15 @@ // Ollama functions ////////////////////////// - const sendPrompt = async (userPrompt, parentId, _chatId) => { + const sendPrompt = async (prompt, parentId) => { + const _chatId = JSON.parse(JSON.stringify($chatId)); await Promise.all( selectedModels.map(async (model) => { console.log(model); if ($models.filter((m) => m.name === model)[0].external) { - await sendPromptOpenAI(model, userPrompt, parentId, _chatId); + await sendPromptOpenAI(model, prompt, parentId, _chatId); } else { - await sendPromptOllama(model, userPrompt, parentId, _chatId); + await sendPromptOllama(model, prompt, parentId, _chatId); } }) ); @@ -297,8 +293,11 @@ if (autoScroll) { window.scrollTo({ top: document.body.scrollHeight }); } + } - await $db.updateChatById(_chatId, { + if ($chatId == _chatId) { + chat = await $db.updateChatById(_chatId, { + ...chat.chat, title: title === '' ? 'New Chat' : title, models: selectedModels, system: $settings.system ?? undefined, @@ -481,8 +480,11 @@ if (autoScroll) { window.scrollTo({ top: document.body.scrollHeight }); } + } - await $db.updateChatById(_chatId, { + if ($chatId == _chatId) { + chat = await $db.updateChatById(_chatId, { + ...chat.chat, title: title === '' ? 'New Chat' : title, models: selectedModels, system: $settings.system ?? undefined, @@ -542,8 +544,7 @@ }; const submitPrompt = async (userPrompt) => { - const _chatId = JSON.parse(JSON.stringify($chatId)); - console.log('submitPrompt', _chatId); + console.log('submitPrompt', $chatId); if (selectedModels.includes('')) { toast.error('Model not selected'); @@ -570,9 +571,10 @@ history.currentId = userMessageId; await tick(); + if (messages.length == 1) { - await $db.createNewChat({ - id: _chatId, + chat = await $db.createNewChat({ + id: $chatId, title: 'New Chat', models: selectedModels, system: $settings.system ?? undefined, @@ -588,6 +590,11 @@ messages: messages, history: history }); + + console.log(chat); + + await chatId.set(chat.id); + await tick(); } prompt = ''; @@ -597,7 +604,7 @@ window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); }, 50); - await sendPrompt(userPrompt, userMessageId, _chatId); + await sendPrompt(userPrompt, userMessageId); } }; @@ -629,7 +636,6 @@ method: 'POST', headers: { 'Content-Type': 'text/event-stream', - ...($settings.authHeader && { Authorization: $settings.authHeader }), ...($user && { Authorization: `Bearer ${localStorage.token}` }) }, body: JSON.stringify({ @@ -659,7 +665,7 @@ }; const setChatTitle = async (_chatId, _title) => { - await $db.updateChatById(_chatId, { title: _title }); + chat = await $db.updateChatById(_chatId, { ...chat.chat, title: _title }); if (_chatId === $chatId) { title = _title; } @@ -672,7 +678,7 @@ }} /> - 0} /> + 0} {initNewChat} />
@@ -681,6 +687,7 @@
modelfile.tagName === selectedModels[0])[0] : null; + let chat = null; + let title = ''; let prompt = ''; let files = []; @@ -53,10 +55,8 @@ $: if ($page.params.id) { (async () => { - let chat = await loadChat(); - - await tick(); - if (chat) { + if (await loadChat()) { + await tick(); loaded = true; } else { await goto('/'); @@ -70,33 +70,38 @@ const loadChat = async () => { await chatId.set($page.params.id); - const chat = await $db.getChatById($chatId); + chat = await $db.getChatById($chatId); - if (chat) { - console.log(chat); + const chatContent = chat.chat; - selectedModels = (chat?.models ?? undefined) !== undefined ? chat.models : [chat.model ?? '']; + if (chatContent) { + console.log(chatContent); + + selectedModels = + (chatContent?.models ?? undefined) !== undefined + ? chatContent.models + : [chatContent.model ?? '']; history = - (chat?.history ?? undefined) !== undefined - ? chat.history - : convertMessagesToHistory(chat.messages); - title = chat.title; + (chatContent?.history ?? undefined) !== undefined + ? chatContent.history + : convertMessagesToHistory(chatContent.messages); + title = chatContent.title; let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); await settings.set({ ..._settings, - system: chat.system ?? _settings.system, - options: chat.options ?? _settings.options + system: chatContent.system ?? _settings.system, + options: chatContent.options ?? _settings.options }); autoScroll = true; - await tick(); + if (messages.length > 0) { history.messages[messages.at(-1).id].done = true; } await tick(); - return chat; + return true; } else { return null; } @@ -141,14 +146,15 @@ // Ollama functions ////////////////////////// - const sendPrompt = async (userPrompt, parentId, _chatId) => { + const sendPrompt = async (prompt, parentId) => { + const _chatId = JSON.parse(JSON.stringify($chatId)); await Promise.all( selectedModels.map(async (model) => { console.log(model); if ($models.filter((m) => m.name === model)[0].external) { - await sendPromptOpenAI(model, userPrompt, parentId, _chatId); + await sendPromptOpenAI(model, prompt, parentId, _chatId); } else { - await sendPromptOllama(model, userPrompt, parentId, _chatId); + await sendPromptOllama(model, prompt, parentId, _chatId); } }) ); @@ -311,8 +317,11 @@ if (autoScroll) { window.scrollTo({ top: document.body.scrollHeight }); } + } - await $db.updateChatById(_chatId, { + if ($chatId == _chatId) { + chat = await $db.updateChatById(_chatId, { + ...chat.chat, title: title === '' ? 'New Chat' : title, models: selectedModels, system: $settings.system ?? undefined, @@ -495,8 +504,11 @@ if (autoScroll) { window.scrollTo({ top: document.body.scrollHeight }); } + } - await $db.updateChatById(_chatId, { + if ($chatId == _chatId) { + chat = await $db.updateChatById(_chatId, { + ...chat.chat, title: title === '' ? 'New Chat' : title, models: selectedModels, system: $settings.system ?? undefined, @@ -556,8 +568,7 @@ }; const submitPrompt = async (userPrompt) => { - const _chatId = JSON.parse(JSON.stringify($chatId)); - console.log('submitPrompt', _chatId); + console.log('submitPrompt', $chatId); if (selectedModels.includes('')) { toast.error('Model not selected'); @@ -584,9 +595,10 @@ history.currentId = userMessageId; await tick(); + if (messages.length == 1) { - await $db.createNewChat({ - id: _chatId, + chat = await $db.createNewChat({ + id: $chatId, title: 'New Chat', models: selectedModels, system: $settings.system ?? undefined, @@ -602,6 +614,11 @@ messages: messages, history: history }); + + console.log(chat); + + await chatId.set(chat.id); + await tick(); } prompt = ''; @@ -611,7 +628,7 @@ window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); }, 50); - await sendPrompt(userPrompt, userMessageId, _chatId); + await sendPrompt(userPrompt, userMessageId); } }; @@ -673,7 +690,10 @@ }; const setChatTitle = async (_chatId, _title) => { - await $db.updateChatById(_chatId, { title: _title }); + chat = await $db.updateChatById(_chatId, { + ...chat.chat, + title: _title + }); if (_chatId === $chatId) { title = _title; } @@ -687,7 +707,13 @@ /> {#if loaded} - 0} /> + 0} + initNewChat={() => { + goto('/'); + }} + />
@@ -696,6 +722,7 @@
{ - const res = await fetch(`${WEBUI_API_BASE_URL}/users/`, { + const res = await fetch(`${WEBUI_API_BASE_URL}/users`, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -58,7 +58,7 @@ }; onMount(async () => { - if ($config === null || !$config.auth || ($config.auth && $user && $user.role !== 'admin')) { + if ($user?.role !== 'admin') { await goto('/'); } else { await getUsers(); diff --git a/src/routes/auth/+page.svelte b/src/routes/auth/+page.svelte index 9ec0b16f..d77ee503 100644 --- a/src/routes/auth/+page.svelte +++ b/src/routes/auth/+page.svelte @@ -105,7 +105,7 @@
-
+
-
+
Ollama WebUI Backend Required
- Oops! It seems like your Ollama WebUI needs a little attention. - describe troubleshooting/installation, help @ discord - - + Oops! You're using an unsupported method (frontend only). + Please access the WebUI from the backend. See readme.md for instructions or join our Discord + for help. +
From 1303407f53129a580f66fe48e091c0396d908473 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Tue, 26 Dec 2023 10:41:55 -0800 Subject: [PATCH 08/43] feat: update chat --- backend/apps/web/models/chats.py | 18 ++++++++++++++++++ backend/apps/web/routers/chats.py | 5 ++++- src/lib/components/chat/Messages.svelte | 1 + 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/backend/apps/web/models/chats.py b/backend/apps/web/models/chats.py index ad9a2877..212a5573 100644 --- a/backend/apps/web/models/chats.py +++ b/backend/apps/web/models/chats.py @@ -44,6 +44,10 @@ class ChatForm(BaseModel): chat: dict +class ChatTitleForm(BaseModel): + title: str + + class ChatResponse(BaseModel): id: str user_id: str @@ -93,6 +97,20 @@ class ChatTable: except: return None + def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]: + try: + query = Chat.update( + chat=json.dumps(chat), + title=chat["title"] if "title" in chat else "New Chat", + timestamp=int(time.time()), + ).where(Chat.id == id) + query.execute() + + chat = Chat.get(Chat.id == id) + return ChatModel(**model_to_dict(chat)) + except: + return None + def get_chat_lists_by_user_id( self, user_id: str, skip: int = 0, limit: int = 50 ) -> List[ChatModel]: diff --git a/backend/apps/web/routers/chats.py b/backend/apps/web/routers/chats.py index 3191db8b..ccb3a32b 100644 --- a/backend/apps/web/routers/chats.py +++ b/backend/apps/web/routers/chats.py @@ -11,6 +11,7 @@ from apps.web.models.users import Users from apps.web.models.chats import ( ChatModel, ChatResponse, + ChatTitleForm, ChatForm, ChatTitleIdResponse, Chats, @@ -95,7 +96,9 @@ async def update_chat_by_id(id: str, form_data: ChatForm, cred=Depends(bearer_sc if user: chat = Chats.get_chat_by_id_and_user_id(id, user.id) if chat: - chat = Chats.update_chat_by_id(id, form_data.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)}) else: raise HTTPException( diff --git a/src/lib/components/chat/Messages.svelte b/src/lib/components/chat/Messages.svelte index 072ade46..6b6e66eb 100644 --- a/src/lib/components/chat/Messages.svelte +++ b/src/lib/components/chat/Messages.svelte @@ -254,6 +254,7 @@ }; const rateMessage = async (messageIdx, rating) => { + // TODO: Move this function to parent messages = messages.map((message, idx) => { if (messageIdx === idx) { message.rating = rating; From bb190245f70322280d8b1f35197cae2999bda399 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Tue, 26 Dec 2023 11:00:56 -0800 Subject: [PATCH 09/43] chore: admin page refac --- src/lib/apis/users/index.ts | 58 ++++++++++++++++++++++++++++++++++ src/routes/admin/+page.svelte | 59 +++++++---------------------------- 2 files changed, 70 insertions(+), 47 deletions(-) create mode 100644 src/lib/apis/users/index.ts diff --git a/src/lib/apis/users/index.ts b/src/lib/apis/users/index.ts new file mode 100644 index 00000000..201880ca --- /dev/null +++ b/src/lib/apis/users/index.ts @@ -0,0 +1,58 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const updateUserRole = async (token: string, id: string, role: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/update/role`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + id: id, + role: role + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((error) => { + console.log(error); + error = error.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getUsers = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((error) => { + console.log(error); + return null; + }); + + if (error) { + throw error; + } + + return res ? res : []; +}; diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte index 5c2fdb87..13078b3c 100644 --- a/src/routes/admin/+page.svelte +++ b/src/routes/admin/+page.svelte @@ -6,62 +6,27 @@ import toast from 'svelte-french-toast'; + import { updateUserRole, getUsers } from '$lib/apis/users'; + let loaded = false; let users = []; - const updateUserRole = async (id, role) => { - const res = await fetch(`${WEBUI_API_BASE_URL}/users/update/role`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${localStorage.token}` - }, - body: JSON.stringify({ - id: id, - role: role - }) - }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((error) => { - console.log(error); - toast.error(error.detail); - return null; - }); + const updateRoleHandler = async (id, role) => { + const res = await updateUserRole(localStorage.token, id, role).catch((error) => { + toast.error(error); + return null; + }); if (res) { - await getUsers(); + users = await getUsers(localStorage.token); } }; - const getUsers = async () => { - const res = await fetch(`${WEBUI_API_BASE_URL}/users`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${localStorage.token}` - } - }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((error) => { - console.log(error); - toast.error(error.detail); - return null; - }); - - users = res ? res : []; - }; - onMount(async () => { if ($user?.role !== 'admin') { await goto('/'); } else { - await getUsers(); + users = await getUsers(localStorage.token); } loaded = true; }); @@ -115,11 +80,11 @@ class=" dark:text-white underline" on:click={() => { if (user.role === 'user') { - updateUserRole(user.id, 'admin'); + updateRoleHandler(user.id, 'admin'); } else if (user.role === 'pending') { - updateUserRole(user.id, 'user'); + updateRoleHandler(user.id, 'user'); } else { - updateUserRole(user.id, 'pending'); + updateRoleHandler(user.id, 'pending'); } }}>{user.role} From 5304ddaa82878ac4cdfd13335344509ef24ddd78 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Tue, 26 Dec 2023 11:02:03 -0800 Subject: [PATCH 10/43] chore: admin page moved --- src/routes/{ => (app)}/admin/+page.svelte | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/routes/{ => (app)}/admin/+page.svelte (100%) diff --git a/src/routes/admin/+page.svelte b/src/routes/(app)/admin/+page.svelte similarity index 100% rename from src/routes/admin/+page.svelte rename to src/routes/(app)/admin/+page.svelte From 753327522af8bbfb2fabce8f5ef2258e1aecde0b Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Tue, 26 Dec 2023 11:22:09 -0800 Subject: [PATCH 11/43] chore: auth page refac --- src/lib/apis/auths/index.ts | 63 ++++++++++++++++++ src/routes/auth/+page.svelte | 125 ++++++++++++++--------------------- 2 files changed, 113 insertions(+), 75 deletions(-) create mode 100644 src/lib/apis/auths/index.ts diff --git a/src/lib/apis/auths/index.ts b/src/lib/apis/auths/index.ts new file mode 100644 index 00000000..5da98e62 --- /dev/null +++ b/src/lib/apis/auths/index.ts @@ -0,0 +1,63 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const userSignIn = async (email: string, password: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signin`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: email, + password: password + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((error) => { + console.log(error); + + error = error.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const userSignUp = async (name: string, email: string, password: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + email: email, + password: password + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((error) => { + console.log(error); + error = error.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/routes/auth/+page.svelte b/src/routes/auth/+page.svelte index d77ee503..451c746d 100644 --- a/src/routes/auth/+page.svelte +++ b/src/routes/auth/+page.svelte @@ -1,5 +1,6 @@ -{#if loaded && $config && $config.auth} +{#if loaded}
@@ -91,7 +67,7 @@
-