diff --git a/example.env b/.env.example similarity index 83% rename from example.env rename to .env.example index 4a4fdaa6c..de763f31c 100644 --- a/example.env +++ b/.env.example @@ -5,6 +5,8 @@ OLLAMA_API_BASE_URL='http://localhost:11434/api' OPENAI_API_BASE_URL='' OPENAI_API_KEY='' +# AUTOMATIC1111_BASE_URL="http://localhost:7860" + # DO NOT TRACK SCARF_NO_ANALYTICS=true DO_NOT_TRACK=true \ No newline at end of file diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 000000000..fa3fa296d --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,49 @@ +name: Release + +on: + push: + branches: + - main # or whatever branch you want to use + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Check for changes in package.json + run: | + git diff --cached --diff-filter=d package.json || { + 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: Create GitHub release + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const release = await github.rest.repos.createRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: `v${{ steps.get_version.outputs.version }}`, + name: `v${{ steps.get_version.outputs.version }}`, + body: 'Automatically created new release', + }) + console.log(`Created release ${release.data.html_url}`) + + - name: Upload package to GitHub release + uses: actions/upload-artifact@v3 + with: + name: package + path: . + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Dockerfile b/Dockerfile index 520c2964d..03dccefe3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,10 +30,24 @@ ENV WEBUI_SECRET_KEY "" ENV SCARF_NO_ANALYTICS true ENV DO_NOT_TRACK true -#Whisper TTS Settings +######## Preloaded models ######## +# whisper TTS Settings ENV WHISPER_MODEL="base" ENV WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models" +# RAG Embedding Model Settings +# any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers +# Leaderboard: https://huggingface.co/spaces/mteb/leaderboard +# for better persormance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB) +# IMPORTANT: If you change the default model (all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them. +ENV RAG_EMBEDDING_MODEL="all-MiniLM-L6-v2" +# device type for whisper tts and ebbeding models - "cpu" (default), "cuda" (nvidia gpu and CUDA required) or "mps" (apple silicon) - choosing this right can lead to better performance +ENV RAG_EMBEDDING_MODEL_DEVICE_TYPE="cpu" +ENV RAG_EMBEDDING_MODEL_DIR="/app/backend/data/cache/embedding/models" +ENV SENTENCE_TRANSFORMERS_HOME $RAG_EMBEDDING_MODEL_DIR + +######## Preloaded models ######## + WORKDIR /app/backend # install python dependencies @@ -48,9 +62,10 @@ RUN apt-get update \ && apt-get install -y pandoc netcat-openbsd \ && rm -rf /var/lib/apt/lists/* -# RUN python -c "from sentence_transformers import SentenceTransformer; model = SentenceTransformer('all-MiniLM-L6-v2')" -RUN python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])" - +# preload embedding model +RUN python -c "import os; from chromadb.utils import embedding_functions; sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction(model_name=os.environ['RAG_EMBEDDING_MODEL'], device=os.environ['RAG_EMBEDDING_MODEL_DEVICE_TYPE'])" +# preload tts model +RUN python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='auto', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])" # copy embedding weight from build RUN mkdir -p /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2 diff --git a/README.md b/README.md index 466e3af62..c5cc439b5 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ # Open WebUI (Formerly Ollama WebUI) 👋 - - - - - - - + + + + + + +  [](https://discord.gg/5rJgQTnV4s) [](https://github.com/sponsors/tjbck) -ChatGPT-Style Web Interface for Ollama 🦙 +User-friendly WebUI for LLMs, Inspired by ChatGPT  @@ -83,11 +83,11 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open 🌟 **Important Note on User Roles and Privacy:** -- **Admin Creation:** The very first account to sign up on the Open WebUI will be granted **Administrator privileges**. This account will have comprehensive control over the platform, including user management and system settings. +- **Admin Creation:** The very first account to sign up on Open WebUI will be granted **Administrator privileges**. This account will have comprehensive control over the platform, including user management and system settings. - **User Registrations:** All subsequent users signing up will initially have their accounts set to **Pending** status by default. These accounts will require approval from the Administrator to gain access to the platform functionalities. -- **Privacy and Data Security:** We prioritize your privacy and data security above all. Please be reassured that all data entered into the Open WebUI is stored locally on your device. Our system is designed to be privacy-first, ensuring that no external requests are made, and your data does not leave your local environment. We are committed to maintaining the highest standards of data privacy and security, ensuring that your information remains confidential and under your control. +- **Privacy and Data Security:** We prioritize your privacy and data security above all. Please be reassured that all data entered into Open WebUI is stored locally on your device. Our system is designed to be privacy-first, ensuring that no external requests are made, and your data does not leave your local environment. We are committed to maintaining the highest standards of data privacy and security, ensuring that your information remains confidential and under your control. ### Steps to Install Open WebUI @@ -212,14 +212,13 @@ For other ways to install, like using Kustomize or Helm, check out [INSTALLATION ### Updating your Docker Installation -In case you want to update your local Docker installation to the latest version, you can do it performing the following actions: +In case you want to update your local Docker installation to the latest version, you can do it with [Watchtower](https://containrrr.dev/watchtower/): ```bash -docker rm -f open-webui -docker pull ghcr.io/open-webui/open-webui:main -[insert command you used to install] +docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --run-once open-webui ``` -In the last line, you need to use the very same command you used to install (local install, remote server, etc.) + +In the last part of the command, replace `open-webui` with your container name if it is different. ### Moving from Ollama WebUI to Open WebUI @@ -257,17 +256,13 @@ Once you verify that all the data has been migrated you can erase the old volume docker volume rm ollama-webui ``` - - - - ## How to Install Without Docker While we strongly recommend using our convenient Docker container installation for optimal support, we understand that some situations may require a non-Docker setup, especially for development purposes. Please note that non-Docker installations are not officially supported, and you might need to troubleshoot on your own. ### Project Components -The Open WebUI consists of two primary components: the frontend and the backend (which serves as a reverse proxy, handling static frontend files, and additional features). Both need to be running concurrently for the development environment. +Open WebUI consists of two primary components: the frontend and the backend (which serves as a reverse proxy, handling static frontend files, and additional features). Both need to be running concurrently for the development environment. > [!IMPORTANT] > The backend is required for proper functionality @@ -286,7 +281,7 @@ git clone https://github.com/open-webui/open-webui.git cd open-webui/ # Copying required .env file -cp -RPp example.env .env +cp -RPp .env.example .env # Building Frontend Using Node npm i @@ -302,7 +297,7 @@ pip install -r requirements.txt -U sh start.sh ``` -You should have the Open WebUI up and running at http://localhost:8080/. Enjoy! 😄 +You should have Open WebUI up and running at http://localhost:8080/. Enjoy! 😄 ## Troubleshooting diff --git a/backend/.gitignore b/backend/.gitignore index 4dd0b8492..16180123c 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -7,4 +7,5 @@ uploads _test Pipfile data/* +!data/config.json .webui_secret_key \ No newline at end of file diff --git a/backend/apps/audio/main.py b/backend/apps/audio/main.py index 86e79c473..d8cb415fc 100644 --- a/backend/apps/audio/main.py +++ b/backend/apps/audio/main.py @@ -56,7 +56,7 @@ def transcribe( model = WhisperModel( WHISPER_MODEL, - device="cpu", + device="auto", compute_type="int8", download_root=WHISPER_MODEL_DIR, ) diff --git a/backend/apps/images/main.py b/backend/apps/images/main.py new file mode 100644 index 000000000..998af3ddb --- /dev/null +++ b/backend/apps/images/main.py @@ -0,0 +1,165 @@ +import os +import requests +from fastapi import ( + FastAPI, + Request, + Depends, + HTTPException, + status, + UploadFile, + File, + Form, +) +from fastapi.middleware.cors import CORSMiddleware +from faster_whisper import WhisperModel + +from constants import ERROR_MESSAGES +from utils.utils import ( + get_current_user, + get_admin_user, +) +from utils.misc import calculate_sha256 +from typing import Optional +from pydantic import BaseModel +from config import AUTOMATIC1111_BASE_URL + +app = FastAPI() +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL +app.state.ENABLED = app.state.AUTOMATIC1111_BASE_URL != "" + + +@app.get("/enabled", response_model=bool) +async def get_enable_status(request: Request, user=Depends(get_admin_user)): + return app.state.ENABLED + + +@app.get("/enabled/toggle", response_model=bool) +async def toggle_enabled(request: Request, user=Depends(get_admin_user)): + try: + r = requests.head(app.state.AUTOMATIC1111_BASE_URL) + app.state.ENABLED = not app.state.ENABLED + return app.state.ENABLED + except Exception as e: + raise HTTPException(status_code=r.status_code, detail=ERROR_MESSAGES.DEFAULT(e)) + + +class UrlUpdateForm(BaseModel): + url: str + + +@app.get("/url") +async def get_openai_url(user=Depends(get_admin_user)): + return {"AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL} + + +@app.post("/url/update") +async def update_openai_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)): + + if form_data.url == "": + app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL + else: + app.state.AUTOMATIC1111_BASE_URL = form_data.url.strip("/") + + return { + "AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL, + "status": True, + } + + +@app.get("/models") +def get_models(user=Depends(get_current_user)): + try: + r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models") + models = r.json() + return models + except Exception as e: + raise HTTPException(status_code=r.status_code, detail=ERROR_MESSAGES.DEFAULT(e)) + + +@app.get("/models/default") +async def get_default_model(user=Depends(get_admin_user)): + try: + r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options") + options = r.json() + + return {"model": options["sd_model_checkpoint"]} + except Exception as e: + raise HTTPException(status_code=r.status_code, detail=ERROR_MESSAGES.DEFAULT(e)) + + +class UpdateModelForm(BaseModel): + model: str + + +def set_model_handler(model: str): + r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options") + options = r.json() + + if model != options["sd_model_checkpoint"]: + options["sd_model_checkpoint"] = model + r = requests.post( + url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options", json=options + ) + + return options + + +@app.post("/models/default/update") +def update_default_model( + form_data: UpdateModelForm, + user=Depends(get_current_user), +): + return set_model_handler(form_data.model) + + +class GenerateImageForm(BaseModel): + model: Optional[str] = None + prompt: str + n: int = 1 + size: str = "512x512" + negative_prompt: Optional[str] = None + + +@app.post("/generations") +def generate_image( + form_data: GenerateImageForm, + user=Depends(get_current_user), +): + + print(form_data) + + try: + if form_data.model: + set_model_handler(form_data.model) + + width, height = tuple(map(int, form_data.size.split("x"))) + + data = { + "prompt": form_data.prompt, + "batch_size": form_data.n, + "width": width, + "height": height, + } + + if form_data.negative_prompt != None: + data["negative_prompt"] = form_data.negative_prompt + + print(data) + + r = requests.post( + url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img", + json=data, + ) + + return r.json() + except Exception as e: + print(e) + raise HTTPException(status_code=r.status_code, detail=ERROR_MESSAGES.DEFAULT(e)) diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py index 6d06456f8..4176d5670 100644 --- a/backend/apps/rag/main.py +++ b/backend/apps/rag/main.py @@ -1,6 +1,5 @@ from fastapi import ( FastAPI, - Request, Depends, HTTPException, status, @@ -14,7 +13,8 @@ import os, shutil from pathlib import Path from typing import List -# from chromadb.utils import embedding_functions +from sentence_transformers import SentenceTransformer +from chromadb.utils import embedding_functions from langchain_community.document_loaders import ( WebBaseLoader, @@ -30,16 +30,12 @@ from langchain_community.document_loaders import ( UnstructuredExcelLoader, ) from langchain.text_splitter import RecursiveCharacterTextSplitter -from langchain.chains import RetrievalQA -from langchain_community.vectorstores import Chroma - from pydantic import BaseModel from typing import Optional import mimetypes import uuid import json -import time from apps.web.models.documents import ( @@ -58,23 +54,37 @@ from utils.utils import get_current_user, get_admin_user from config import ( UPLOAD_DIR, DOCS_DIR, - EMBED_MODEL, + RAG_EMBEDDING_MODEL, + RAG_EMBEDDING_MODEL_DEVICE_TYPE, CHROMA_CLIENT, CHUNK_SIZE, CHUNK_OVERLAP, RAG_TEMPLATE, ) + from constants import ERROR_MESSAGES -# EMBEDDING_FUNC = embedding_functions.SentenceTransformerEmbeddingFunction( -# model_name=EMBED_MODEL -# ) +# +# if RAG_EMBEDDING_MODEL: +# sentence_transformer_ef = SentenceTransformer( +# model_name_or_path=RAG_EMBEDDING_MODEL, +# cache_folder=RAG_EMBEDDING_MODEL_DIR, +# device=RAG_EMBEDDING_MODEL_DEVICE_TYPE, +# ) + app = FastAPI() app.state.CHUNK_SIZE = CHUNK_SIZE app.state.CHUNK_OVERLAP = CHUNK_OVERLAP app.state.RAG_TEMPLATE = RAG_TEMPLATE +app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL +app.state.sentence_transformer_ef = ( + embedding_functions.SentenceTransformerEmbeddingFunction( + model_name=app.state.RAG_EMBEDDING_MODEL, + device=RAG_EMBEDDING_MODEL_DEVICE_TYPE, + ) +) origins = ["*"] @@ -106,7 +116,10 @@ def store_data_in_vector_db(data, collection_name) -> bool: metadatas = [doc.metadata for doc in docs] try: - collection = CHROMA_CLIENT.create_collection(name=collection_name) + collection = CHROMA_CLIENT.create_collection( + name=collection_name, + embedding_function=app.state.sentence_transformer_ef, + ) collection.add( documents=texts, metadatas=metadatas, ids=[str(uuid.uuid1()) for _ in texts] @@ -126,6 +139,38 @@ async def get_status(): "status": True, "chunk_size": app.state.CHUNK_SIZE, "chunk_overlap": app.state.CHUNK_OVERLAP, + "template": app.state.RAG_TEMPLATE, + "embedding_model": app.state.RAG_EMBEDDING_MODEL, + } + + +@app.get("/embedding/model") +async def get_embedding_model(user=Depends(get_admin_user)): + return { + "status": True, + "embedding_model": app.state.RAG_EMBEDDING_MODEL, + } + + +class EmbeddingModelUpdateForm(BaseModel): + embedding_model: str + + +@app.post("/embedding/model/update") +async def update_embedding_model( + form_data: EmbeddingModelUpdateForm, user=Depends(get_admin_user) +): + app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model + app.state.sentence_transformer_ef = ( + embedding_functions.SentenceTransformerEmbeddingFunction( + model_name=app.state.RAG_EMBEDDING_MODEL, + device=RAG_EMBEDDING_MODEL_DEVICE_TYPE, + ) + ) + + return { + "status": True, + "embedding_model": app.state.RAG_EMBEDDING_MODEL, } @@ -190,8 +235,10 @@ def query_doc( user=Depends(get_current_user), ): try: + # if you use docker use the model from the environment variable collection = CHROMA_CLIENT.get_collection( name=form_data.collection_name, + embedding_function=app.state.sentence_transformer_ef, ) result = collection.query(query_texts=[form_data.query], n_results=form_data.k) return result @@ -263,9 +310,12 @@ def query_collection( for collection_name in form_data.collection_names: try: + # if you use docker use the model from the environment variable collection = CHROMA_CLIENT.get_collection( name=collection_name, + embedding_function=app.state.sentence_transformer_ef, ) + result = collection.query( query_texts=[form_data.query], n_results=form_data.k ) diff --git a/backend/apps/web/main.py b/backend/apps/web/main.py index 400ddac0d..bd14f4bda 100644 --- a/backend/apps/web/main.py +++ b/backend/apps/web/main.py @@ -26,6 +26,8 @@ app = FastAPI() origins = ["*"] app.state.ENABLE_SIGNUP = ENABLE_SIGNUP +app.state.JWT_EXPIRES_IN = "-1" + app.state.DEFAULT_MODELS = DEFAULT_MODELS app.state.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS app.state.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE @@ -55,7 +57,6 @@ app.include_router(utils.router, prefix="/utils", tags=["utils"]) async def get_status(): return { "status": True, - "version": WEBUI_VERSION, "auth": WEBUI_AUTH, "default_models": app.state.DEFAULT_MODELS, "default_prompt_suggestions": app.state.DEFAULT_PROMPT_SUGGESTIONS, diff --git a/backend/apps/web/routers/auths.py b/backend/apps/web/routers/auths.py index 7ccef6300..3db2d0ad2 100644 --- a/backend/apps/web/routers/auths.py +++ b/backend/apps/web/routers/auths.py @@ -7,6 +7,7 @@ from fastapi import APIRouter, status from pydantic import BaseModel import time import uuid +import re from apps.web.models.auths import ( SigninForm, @@ -25,7 +26,7 @@ from utils.utils import ( get_admin_user, create_token, ) -from utils.misc import get_gravatar_url, validate_email_format +from utils.misc import parse_duration, validate_email_format from constants import ERROR_MESSAGES router = APIRouter() @@ -95,10 +96,13 @@ async def update_password( @router.post("/signin", response_model=SigninResponse) -async def signin(form_data: SigninForm): +async def signin(request: Request, form_data: SigninForm): user = Auths.authenticate_user(form_data.email.lower(), form_data.password) if user: - token = create_token(data={"id": user.id}) + token = create_token( + data={"id": user.id}, + expires_delta=parse_duration(request.app.state.JWT_EXPIRES_IN), + ) return { "token": token, @@ -145,7 +149,10 @@ async def signup(request: Request, form_data: SignupForm): ) if user: - token = create_token(data={"id": user.id}) + token = create_token( + data={"id": user.id}, + expires_delta=parse_duration(request.app.state.JWT_EXPIRES_IN), + ) # response.set_cookie(key='token', value=token, httponly=True) return { @@ -200,3 +207,33 @@ async def update_default_user_role( if form_data.role in ["pending", "user", "admin"]: request.app.state.DEFAULT_USER_ROLE = form_data.role return request.app.state.DEFAULT_USER_ROLE + + +############################ +# JWT Expiration +############################ + + +@router.get("/token/expires") +async def get_token_expires_duration(request: Request, user=Depends(get_admin_user)): + return request.app.state.JWT_EXPIRES_IN + + +class UpdateJWTExpiresDurationForm(BaseModel): + duration: str + + +@router.post("/token/expires/update") +async def update_token_expires_duration( + request: Request, + form_data: UpdateJWTExpiresDurationForm, + user=Depends(get_admin_user), +): + pattern = r"^(-1|0|(-?\d+(\.\d+)?)(ms|s|m|h|d|w))$" + + # Check if the input string matches the pattern + if re.match(pattern, form_data.duration): + request.app.state.JWT_EXPIRES_IN = form_data.duration + return request.app.state.JWT_EXPIRES_IN + else: + return request.app.state.JWT_EXPIRES_IN diff --git a/backend/config.py b/backend/config.py index 440256c48..caf2cc457 100644 --- a/backend/config.py +++ b/backend/config.py @@ -5,6 +5,8 @@ from secrets import token_bytes from base64 import b64encode from constants import ERROR_MESSAGES from pathlib import Path +import json + try: from dotenv import load_dotenv, find_dotenv @@ -28,6 +30,12 @@ ENV = os.environ.get("ENV", "dev") DATA_DIR = str(Path(os.getenv("DATA_DIR", "./data")).resolve()) FRONTEND_BUILD_DIR = str(Path(os.getenv("FRONTEND_BUILD_DIR", "../build"))) +try: + with open(f"{DATA_DIR}/config.json", "r") as f: + CONFIG_DATA = json.load(f) +except: + CONFIG_DATA = {} + #################################### # File Upload DIR #################################### @@ -80,9 +88,14 @@ if OPENAI_API_BASE_URL == "": ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", True) DEFAULT_MODELS = os.environ.get("DEFAULT_MODELS", None) -DEFAULT_PROMPT_SUGGESTIONS = os.environ.get( - "DEFAULT_PROMPT_SUGGESTIONS", - [ + + +DEFAULT_PROMPT_SUGGESTIONS = ( + CONFIG_DATA["ui"]["prompt_suggestions"] + if "ui" in CONFIG_DATA + and "prompt_suggestions" in CONFIG_DATA["ui"] + and type(CONFIG_DATA["ui"]["prompt_suggestions"]) is list + else [ { "title": ["Help me study", "vocabulary for a college entrance exam"], "content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.", @@ -99,8 +112,10 @@ DEFAULT_PROMPT_SUGGESTIONS = os.environ.get( "title": ["Show me a code snippet", "of a website's sticky header"], "content": "Show me a code snippet of a website's sticky header in CSS and JavaScript.", }, - ], + ] ) + + DEFAULT_USER_ROLE = "pending" USER_PERMISSIONS = {"chat": {"deletion": True}} @@ -136,7 +151,12 @@ if WEBUI_AUTH and WEBUI_SECRET_KEY == "": #################################### CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db" -EMBED_MODEL = "all-MiniLM-L6-v2" +# this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (all-MiniLM-L6-v2) +RAG_EMBEDDING_MODEL = os.environ.get("RAG_EMBEDDING_MODEL", "all-MiniLM-L6-v2") +# device type ebbeding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance +RAG_EMBEDDING_MODEL_DEVICE_TYPE = os.environ.get( + "RAG_EMBEDDING_MODEL_DEVICE_TYPE", "cpu" +) CHROMA_CLIENT = chromadb.PersistentClient( path=CHROMA_DATA_PATH, settings=Settings(allow_reset=True, anonymized_telemetry=False), @@ -165,3 +185,10 @@ Query: [query]""" WHISPER_MODEL = os.getenv("WHISPER_MODEL", "base") WHISPER_MODEL_DIR = os.getenv("WHISPER_MODEL_DIR", f"{CACHE_DIR}/whisper/models") + + +#################################### +# Images +#################################### + +AUTOMATIC1111_BASE_URL = os.getenv("AUTOMATIC1111_BASE_URL", "") diff --git a/backend/data/config.json b/backend/data/config.json new file mode 100644 index 000000000..1b5971005 --- /dev/null +++ b/backend/data/config.json @@ -0,0 +1,34 @@ +{ + "ui": { + "prompt_suggestions": [ + { + "title": [ + "Help me study", + "vocabulary for a college entrance exam" + ], + "content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option." + }, + { + "title": [ + "Give me ideas", + "for what to do with my kids' art" + ], + "content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter." + }, + { + "title": [ + "Tell me a fun fact", + "about the Roman Empire" + ], + "content": "Tell me a random fun fact about the Roman Empire" + }, + { + "title": [ + "Show me a code snippet", + "of a website's sticky header" + ], + "content": "Show me a code snippet of a website's sticky header in CSS and JavaScript." + } + ] + } +} \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 3a28670ef..d1fb0c205 100644 --- a/backend/main.py +++ b/backend/main.py @@ -11,10 +11,10 @@ from starlette.exceptions import HTTPException as StarletteHTTPException from apps.ollama.main import app as ollama_app from apps.openai.main import app as openai_app from apps.audio.main import app as audio_app - +from apps.images.main import app as images_app +from apps.rag.main import app as rag_app from apps.web.main import app as webui_app -from apps.rag.main import app as rag_app from config import ENV, FRONTEND_BUILD_DIR @@ -58,10 +58,21 @@ app.mount("/api/v1", webui_app) app.mount("/ollama/api", ollama_app) app.mount("/openai/api", openai_app) +app.mount("/images/api/v1", images_app) app.mount("/audio/api/v1", audio_app) app.mount("/rag/api/v1", rag_app) +@app.get("/api/config") +async def get_app_config(): + return { + "status": True, + "images": images_app.state.ENABLED, + "default_models": webui_app.state.DEFAULT_MODELS, + "default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS, + } + + app.mount( "/", SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True), diff --git a/backend/utils/misc.py b/backend/utils/misc.py index 5e9d5876e..98528c400 100644 --- a/backend/utils/misc.py +++ b/backend/utils/misc.py @@ -1,6 +1,8 @@ from pathlib import Path import hashlib import re +from datetime import timedelta +from typing import Optional def get_gravatar_url(email): @@ -76,3 +78,34 @@ def extract_folders_after_data_docs(path): tags.append("/".join(folders[: idx + 1])) return tags + + +def parse_duration(duration: str) -> Optional[timedelta]: + if duration == "-1" or duration == "0": + return None + + # Regular expression to find number and unit pairs + pattern = r"(-?\d+(\.\d+)?)(ms|s|m|h|d|w)" + matches = re.findall(pattern, duration) + + if not matches: + raise ValueError("Invalid duration string") + + total_duration = timedelta() + + for number, _, unit in matches: + number = float(number) + if unit == "ms": + total_duration += timedelta(milliseconds=number) + elif unit == "s": + total_duration += timedelta(seconds=number) + elif unit == "m": + total_duration += timedelta(minutes=number) + elif unit == "h": + total_duration += timedelta(hours=number) + elif unit == "d": + total_duration += timedelta(days=number) + elif unit == "w": + total_duration += timedelta(weeks=number) + + return total_duration diff --git a/demo.gif b/demo.gif index 014f40e2b..4c2fbd539 100644 Binary files a/demo.gif and b/demo.gif differ diff --git a/kubernetes/helm/Chart.yaml b/kubernetes/helm/Chart.yaml index 52683b65e..c35338c84 100644 --- a/kubernetes/helm/Chart.yaml +++ b/kubernetes/helm/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v2 -name: ollama-webui -description: "Ollama Web UI: A User-Friendly Web Interface for Chat Interactions 👋" +name: open-webui +description: "Open WebUI: A User-Friendly Web Interface for Chat Interactions 👋" version: 1.0.0 -icon: https://raw.githubusercontent.com/ollama-webui/ollama-webui/main/static/favicon.png +icon: https://raw.githubusercontent.com/open-webui/open-webui/main/static/favicon.png diff --git a/kubernetes/helm/templates/webui-deployment.yaml b/kubernetes/helm/templates/webui-deployment.yaml index d9721ee05..08c966886 100644 --- a/kubernetes/helm/templates/webui-deployment.yaml +++ b/kubernetes/helm/templates/webui-deployment.yaml @@ -1,20 +1,20 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: ollama-webui-deployment + name: open-webui-deployment namespace: {{ .Values.namespace }} spec: replicas: 1 selector: matchLabels: - app: ollama-webui + app: open-webui template: metadata: labels: - app: ollama-webui + app: open-webui spec: containers: - - name: ollama-webui + - name: open-webui image: {{ .Values.webui.image }} ports: - containerPort: 8080 @@ -35,4 +35,4 @@ spec: volumes: - name: webui-volume persistentVolumeClaim: - claimName: ollama-webui-pvc \ No newline at end of file + claimName: open-webui-pvc \ No newline at end of file diff --git a/kubernetes/helm/templates/webui-ingress.yaml b/kubernetes/helm/templates/webui-ingress.yaml index 84f819f37..cbd456d3f 100644 --- a/kubernetes/helm/templates/webui-ingress.yaml +++ b/kubernetes/helm/templates/webui-ingress.yaml @@ -2,7 +2,7 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: ollama-webui-ingress + name: open-webui-ingress namespace: {{ .Values.namespace }} {{- if .Values.webui.ingress.annotations }} annotations: @@ -17,7 +17,7 @@ spec: pathType: Prefix backend: service: - name: ollama-webui-service + name: open-webui-service port: number: {{ .Values.webui.servicePort }} {{- end }} diff --git a/kubernetes/helm/templates/webui-pvc.yaml b/kubernetes/helm/templates/webui-pvc.yaml index e9961aa8d..d090fe872 100644 --- a/kubernetes/helm/templates/webui-pvc.yaml +++ b/kubernetes/helm/templates/webui-pvc.yaml @@ -2,8 +2,8 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: labels: - app: ollama-webui - name: ollama-webui-pvc + app: open-webui + name: open-webui-pvc namespace: {{ .Values.namespace }} spec: accessModes: [ "ReadWriteOnce" ] diff --git a/kubernetes/helm/templates/webui-service.yaml b/kubernetes/helm/templates/webui-service.yaml index 7fefa4fd4..afd526a15 100644 --- a/kubernetes/helm/templates/webui-service.yaml +++ b/kubernetes/helm/templates/webui-service.yaml @@ -1,12 +1,12 @@ apiVersion: v1 kind: Service metadata: - name: ollama-webui-service + name: open-webui-service namespace: {{ .Values.namespace }} spec: type: {{ .Values.webui.service.type }} # Default: NodePort # Use LoadBalancer if you're on a cloud that supports it selector: - app: ollama-webui + app: open-webui ports: - protocol: TCP port: {{ .Values.webui.servicePort }} diff --git a/kubernetes/helm/values.yaml b/kubernetes/helm/values.yaml index 648b40509..63781f6cc 100644 --- a/kubernetes/helm/values.yaml +++ b/kubernetes/helm/values.yaml @@ -1,15 +1,18 @@ -namespace: ollama-namespace +namespace: open-webui ollama: replicaCount: 1 image: ollama/ollama:latest servicePort: 11434 resources: - limits: + requests: cpu: "2000m" memory: "2Gi" + limits: + cpu: "4000m" + memory: "4Gi" nvidia.com/gpu: "0" - volumeSize: 1Gi + volumeSize: 30Gi nodeSelector: {} tolerations: [] service: @@ -19,19 +22,22 @@ ollama: webui: replicaCount: 1 - image: ghcr.io/ollama-webui/ollama-webui:main + image: ghcr.io/open-webui/open-webui:main servicePort: 8080 resources: - limits: + requests: cpu: "500m" memory: "500Mi" + limits: + cpu: "1000m" + memory: "1Gi" ingress: enabled: true annotations: # Use appropriate annotations for your Ingress controller, e.g., for NGINX: # nginx.ingress.kubernetes.io/rewrite-target: / - host: ollama.minikube.local - volumeSize: 1Gi + host: open-webui.minikube.local + volumeSize: 2Gi nodeSelector: {} tolerations: [] service: diff --git a/kubernetes/manifest/base/ollama-service.yaml b/kubernetes/manifest/base/ollama-service.yaml index a9467fc44..8bab65b59 100644 --- a/kubernetes/manifest/base/ollama-service.yaml +++ b/kubernetes/manifest/base/ollama-service.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: Service metadata: name: ollama-service - namespace: ollama-namespace + namespace: open-webui spec: selector: app: ollama diff --git a/kubernetes/manifest/base/ollama-statefulset.yaml b/kubernetes/manifest/base/ollama-statefulset.yaml index ee63faa95..cd1144caf 100644 --- a/kubernetes/manifest/base/ollama-statefulset.yaml +++ b/kubernetes/manifest/base/ollama-statefulset.yaml @@ -2,7 +2,7 @@ apiVersion: apps/v1 kind: StatefulSet metadata: name: ollama - namespace: ollama-namespace + namespace: open-webui spec: serviceName: "ollama" replicas: 1 @@ -20,9 +20,13 @@ spec: ports: - containerPort: 11434 resources: - limits: + requests: cpu: "2000m" memory: "2Gi" + limits: + cpu: "4000m" + memory: "4Gi" + nvidia.com/gpu: "0" volumeMounts: - name: ollama-volume mountPath: /root/.ollama @@ -34,4 +38,4 @@ spec: accessModes: [ "ReadWriteOnce" ] resources: requests: - storage: 1Gi \ No newline at end of file + storage: 30Gi \ No newline at end of file diff --git a/kubernetes/manifest/base/ollama-namespace.yaml b/kubernetes/manifest/base/open-webui.yaml similarity index 63% rename from kubernetes/manifest/base/ollama-namespace.yaml rename to kubernetes/manifest/base/open-webui.yaml index f296eb206..9c1a599f3 100644 --- a/kubernetes/manifest/base/ollama-namespace.yaml +++ b/kubernetes/manifest/base/open-webui.yaml @@ -1,4 +1,4 @@ apiVersion: v1 kind: Namespace metadata: - name: ollama-namespace \ No newline at end of file + name: open-webui \ No newline at end of file diff --git a/kubernetes/manifest/base/webui-deployment.yaml b/kubernetes/manifest/base/webui-deployment.yaml index 58de03680..174025a94 100644 --- a/kubernetes/manifest/base/webui-deployment.yaml +++ b/kubernetes/manifest/base/webui-deployment.yaml @@ -1,28 +1,38 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: ollama-webui-deployment - namespace: ollama-namespace + name: open-webui-deployment + namespace: open-webui spec: replicas: 1 selector: matchLabels: - app: ollama-webui + app: open-webui template: metadata: labels: - app: ollama-webui + app: open-webui spec: containers: - - name: ollama-webui - image: ghcr.io/ollama-webui/ollama-webui:main + - name: open-webui + image: ghcr.io/open-webui/open-webui:main ports: - containerPort: 8080 resources: - limits: + requests: cpu: "500m" memory: "500Mi" + limits: + cpu: "1000m" + memory: "1Gi" env: - name: OLLAMA_API_BASE_URL - value: "http://ollama-service.ollama-namespace.svc.cluster.local:11434/api" - tty: true \ No newline at end of file + value: "http://ollama-service.open-webui.svc.cluster.local:11434/api" + tty: true + volumeMounts: + - name: webui-volume + mountPath: /app/backend/data + volumes: + - name: webui-volume + persistentVolumeClaim: + claimName: ollama-webui-pvc \ No newline at end of file diff --git a/kubernetes/manifest/base/webui-ingress.yaml b/kubernetes/manifest/base/webui-ingress.yaml index 0038807cb..dc0b53ccd 100644 --- a/kubernetes/manifest/base/webui-ingress.yaml +++ b/kubernetes/manifest/base/webui-ingress.yaml @@ -1,20 +1,20 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: ollama-webui-ingress - namespace: ollama-namespace + name: open-webui-ingress + namespace: open-webui #annotations: # Use appropriate annotations for your Ingress controller, e.g., for NGINX: # nginx.ingress.kubernetes.io/rewrite-target: / spec: rules: - - host: ollama.minikube.local + - host: open-webui.minikube.local http: paths: - path: / pathType: Prefix backend: service: - name: ollama-webui-service + name: open-webui-service port: number: 8080 diff --git a/kubernetes/manifest/base/webui-pvc.yaml b/kubernetes/manifest/base/webui-pvc.yaml new file mode 100644 index 000000000..285dfeef7 --- /dev/null +++ b/kubernetes/manifest/base/webui-pvc.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + app: ollama-webui + name: ollama-webui-pvc + namespace: ollama-namespace +spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 2Gi \ No newline at end of file diff --git a/kubernetes/manifest/base/webui-service.yaml b/kubernetes/manifest/base/webui-service.yaml index b41daeafb..d73845f00 100644 --- a/kubernetes/manifest/base/webui-service.yaml +++ b/kubernetes/manifest/base/webui-service.yaml @@ -1,12 +1,12 @@ apiVersion: v1 kind: Service metadata: - name: ollama-webui-service - namespace: ollama-namespace + name: open-webui-service + namespace: open-webui spec: type: NodePort # Use LoadBalancer if you're on a cloud that supports it selector: - app: ollama-webui + app: open-webui ports: - protocol: TCP port: 8080 diff --git a/kubernetes/manifest/kustomization.yaml b/kubernetes/manifest/kustomization.yaml index a4b03d961..f581839e8 100644 --- a/kubernetes/manifest/kustomization.yaml +++ b/kubernetes/manifest/kustomization.yaml @@ -1,5 +1,5 @@ resources: -- base/ollama-namespace.yaml +- base/open-webui.yaml - base/ollama-service.yaml - base/ollama-statefulset.yaml - base/webui-deployment.yaml diff --git a/kubernetes/manifest/patches/ollama-statefulset-gpu.yaml b/kubernetes/manifest/patches/ollama-statefulset-gpu.yaml index 54e5aba65..3e4244365 100644 --- a/kubernetes/manifest/patches/ollama-statefulset-gpu.yaml +++ b/kubernetes/manifest/patches/ollama-statefulset-gpu.yaml @@ -2,7 +2,7 @@ apiVersion: apps/v1 kind: StatefulSet metadata: name: ollama - namespace: ollama-namespace + namespace: open-webui spec: selector: matchLabels: diff --git a/package-lock.json b/package-lock.json index 6f962e70d..ded65c9ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "ollama-webui", + "name": "open-webui", "version": "0.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "ollama-webui", + "name": "open-webui", "version": "0.0.1", "dependencies": { "@sveltejs/adapter-node": "^1.3.1", diff --git a/package.json b/package.json index bae50d1b8..352130769 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "ollama-webui", - "version": "0.0.1", + "name": "open-webui", + "version": "0.1.0-101", "private": true, "scripts": { "dev": "vite dev --host", diff --git a/run.sh b/run.sh index c8ac77ccc..6793fe162 100644 --- a/run.sh +++ b/run.sh @@ -1,7 +1,7 @@ #!/bin/bash -image_name="ollama-webui" -container_name="ollama-webui" +image_name="open-webui" +container_name="open-webui" host_port=3000 container_port=8080 diff --git a/src/lib/apis/auths/index.ts b/src/lib/apis/auths/index.ts index 078589984..169998726 100644 --- a/src/lib/apis/auths/index.ts +++ b/src/lib/apis/auths/index.ts @@ -261,3 +261,60 @@ export const toggleSignUpEnabledStatus = async (token: string) => { return res; }; + +export const getJWTExpiresDuration = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/token/expires`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateJWTExpiresDuration = async (token: string, duration: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/token/expires/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + duration: duration + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/images/index.ts b/src/lib/apis/images/index.ts new file mode 100644 index 000000000..b25499d64 --- /dev/null +++ b/src/lib/apis/images/index.ts @@ -0,0 +1,266 @@ +import { IMAGES_API_BASE_URL } from '$lib/constants'; + +export const getImageGenerationEnabledStatus = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/enabled`, { + 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((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const toggleImageGenerationEnabledStatus = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/enabled/toggle`, { + 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((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getAUTOMATIC1111Url = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/url`, { + 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((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.AUTOMATIC1111_BASE_URL; +}; + +export const updateAUTOMATIC1111Url = async (token: string = '', url: string) => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/url/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + url: url + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.AUTOMATIC1111_BASE_URL; +}; + +export const getDiffusionModels = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/models`, { + 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((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getDefaultDiffusionModel = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/models/default`, { + 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((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.model; +}; + +export const updateDefaultDiffusionModel = async (token: string = '', model: string) => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/models/default/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + model: model + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.model; +}; + +export const imageGenerations = async (token: string = '', prompt: string) => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/generations`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + prompt: prompt + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts index 915121661..c20107ce2 100644 --- a/src/lib/apis/index.ts +++ b/src/lib/apis/index.ts @@ -1,9 +1,9 @@ -import { WEBUI_API_BASE_URL } from '$lib/constants'; +import { WEBUI_BASE_URL } from '$lib/constants'; export const getBackendConfig = async () => { let error = null; - const res = await fetch(`${WEBUI_API_BASE_URL}/`, { + const res = await fetch(`${WEBUI_BASE_URL}/api/config`, { method: 'GET', headers: { 'Content-Type': 'application/json' diff --git a/src/lib/apis/ollama/index.ts b/src/lib/apis/ollama/index.ts index 625019660..954956aca 100644 --- a/src/lib/apis/ollama/index.ts +++ b/src/lib/apis/ollama/index.ts @@ -133,9 +133,19 @@ export const getOllamaModels = async (token: string = '') => { }); }; -export const generateTitle = async (token: string = '', model: string, prompt: string) => { +// TODO: migrate to backend +export const generateTitle = async ( + token: string = '', + template: string, + model: string, + prompt: string +) => { let error = null; + template = template.replace(/{{prompt}}/g, prompt); + + console.log(template); + const res = await fetch(`${OLLAMA_API_BASE_URL}/generate`, { method: 'POST', headers: { @@ -144,7 +154,7 @@ export const generateTitle = async (token: string = '', model: string, prompt: s }, body: JSON.stringify({ model: model, - prompt: `Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title': ${prompt}`, + prompt: template, stream: false }) }) diff --git a/src/lib/components/admin/Settings/General.svelte b/src/lib/components/admin/Settings/General.svelte index 48ed41e74..b8f71d30f 100644 --- a/src/lib/components/admin/Settings/General.svelte +++ b/src/lib/components/admin/Settings/General.svelte @@ -1,15 +1,18 @@ @@ -29,6 +37,7 @@ class="flex flex-col h-full justify-between space-y-3 text-sm" on:submit|preventDefault={() => { // console.log('submit'); + updateJWTExpiresDurationHandler(JWTExpiresIn); saveHandler(); }} > @@ -94,6 +103,29 @@ + +