diff --git a/.gitignore b/.gitignore index 528e1f83..2ccac4d5 100644 --- a/.gitignore +++ b/.gitignore @@ -166,7 +166,7 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ # Logs logs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3956e566..da3e289d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.115] - 2024-03-24 + +### Added + +- **🔍 Custom Model Selector**: Easily find and select custom models with the new search filter feature. +- **🛑 Cancel Model Download**: Added the ability to cancel model downloads. +- **🎨 Image Generation ComfyUI**: Image generation now supports ComfyUI. +- **🌟 Updated Light Theme**: Updated the light theme for a fresh look. +- **🌍 Additional Language Support**: Now supporting Bulgarian, Italian, Portuguese, Japanese, and Dutch. + +### Fixed + +- **🔧 Fixed Broken Experimental GGUF Upload**: Resolved issues with experimental GGUF upload functionality. + +### Changed + +- **🔄 Vector Storage Reset Button**: Moved the reset vector storage button to document settings. + ## [0.1.114] - 2024-03-20 ### Added diff --git a/backend/apps/audio/main.py b/backend/apps/audio/main.py index d8cb415f..10cc5671 100644 --- a/backend/apps/audio/main.py +++ b/backend/apps/audio/main.py @@ -1,4 +1,5 @@ import os +import logging from fastapi import ( FastAPI, Request, @@ -21,7 +22,10 @@ from utils.utils import ( ) from utils.misc import calculate_sha256 -from config import CACHE_DIR, UPLOAD_DIR, WHISPER_MODEL, WHISPER_MODEL_DIR +from config import SRC_LOG_LEVELS, CACHE_DIR, UPLOAD_DIR, WHISPER_MODEL, WHISPER_MODEL_DIR + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["AUDIO"]) app = FastAPI() app.add_middleware( @@ -38,7 +42,7 @@ def transcribe( file: UploadFile = File(...), user=Depends(get_current_user), ): - print(file.content_type) + log.info(f"file.content_type: {file.content_type}") if file.content_type not in ["audio/mpeg", "audio/wav"]: raise HTTPException( @@ -62,7 +66,7 @@ def transcribe( ) segments, info = model.transcribe(file_path, beam_size=5) - print( + log.info( "Detected language '%s' with probability %f" % (info.language, info.language_probability) ) @@ -72,7 +76,7 @@ def transcribe( return {"text": transcript.strip()} except Exception as e: - print(e) + log.exception(e) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, diff --git a/backend/apps/images/main.py b/backend/apps/images/main.py index b829b049..d331fa38 100644 --- a/backend/apps/images/main.py +++ b/backend/apps/images/main.py @@ -27,10 +27,14 @@ from pathlib import Path import uuid import base64 import json +import logging -from config import CACHE_DIR, AUTOMATIC1111_BASE_URL, COMFYUI_BASE_URL +from config import SRC_LOG_LEVELS, CACHE_DIR, AUTOMATIC1111_BASE_URL, COMFYUI_BASE_URL +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["IMAGES"]) + IMAGE_CACHE_DIR = Path(CACHE_DIR).joinpath("./image/generations/") IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True) @@ -304,7 +308,7 @@ def save_b64_image(b64_str): return image_id except Exception as e: - print(f"Error saving image: {e}") + log.error(f"Error saving image: {e}") return None @@ -431,7 +435,7 @@ def generate_image( res = r.json() - print(res) + log.debug(f"res: {res}") images = [] diff --git a/backend/apps/litellm/main.py b/backend/apps/litellm/main.py index 838b4707..a9922aad 100644 --- a/backend/apps/litellm/main.py +++ b/backend/apps/litellm/main.py @@ -1,3 +1,5 @@ +import logging + from litellm.proxy.proxy_server import ProxyConfig, initialize from litellm.proxy.proxy_server import app @@ -9,7 +11,10 @@ from starlette.responses import StreamingResponse import json from utils.utils import get_http_authorization_cred, get_current_user -from config import ENV +from config import SRC_LOG_LEVELS, ENV + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["LITELLM"]) from config import ( @@ -49,7 +54,7 @@ async def auth_middleware(request: Request, call_next): try: user = get_current_user(get_http_authorization_cred(auth_header)) - print(user) + log.debug(f"user: {user}") request.state.user = user except Exception as e: return JSONResponse(status_code=400, content={"detail": str(e)}) diff --git a/backend/apps/ollama/main.py b/backend/apps/ollama/main.py index 2a67b02d..f8701c43 100644 --- a/backend/apps/ollama/main.py +++ b/backend/apps/ollama/main.py @@ -23,6 +23,7 @@ import json import uuid import aiohttp import asyncio +import logging from urllib.parse import urlparse from typing import Optional, List, Union @@ -30,11 +31,13 @@ from typing import Optional, List, Union from apps.web.models.users import Users from constants import ERROR_MESSAGES from utils.utils import decode_token, get_current_user, get_admin_user + + +from config import SRC_LOG_LEVELS, OLLAMA_BASE_URLS, MODEL_FILTER_ENABLED, MODEL_FILTER_LIST, UPLOAD_DIR from utils.misc import calculate_sha256 - -from config import OLLAMA_BASE_URLS, MODEL_FILTER_ENABLED, MODEL_FILTER_LIST, UPLOAD_DIR - +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["OLLAMA"]) app = FastAPI() app.add_middleware( @@ -85,7 +88,7 @@ class UrlUpdateForm(BaseModel): async def update_ollama_api_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)): app.state.OLLAMA_BASE_URLS = form_data.urls - print(app.state.OLLAMA_BASE_URLS) + log.info(f"app.state.OLLAMA_BASE_URLS: {app.state.OLLAMA_BASE_URLS}") return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS} @@ -106,7 +109,7 @@ async def fetch_url(url): return await response.json() except Exception as e: # Handle connection error here - print(f"Connection error: {e}") + log.error(f"Connection error: {e}") return None @@ -130,7 +133,7 @@ def merge_models_lists(model_lists): async def get_all_models(): - print("get_all_models") + log.info("get_all_models()") tasks = [fetch_url(f"{url}/api/tags") for url in app.state.OLLAMA_BASE_URLS] responses = await asyncio.gather(*tasks) @@ -171,7 +174,7 @@ async def get_ollama_tags( return r.json() except Exception as e: - print(e) + log.exception(e) error_detail = "Open WebUI: Server Connection Error" if r is not None: try: @@ -217,7 +220,7 @@ async def get_ollama_versions(url_idx: Optional[int] = None): return r.json() except Exception as e: - print(e) + log.exception(e) error_detail = "Open WebUI: Server Connection Error" if r is not None: try: @@ -243,18 +246,33 @@ async def pull_model( form_data: ModelNameForm, url_idx: int = 0, user=Depends(get_admin_user) ): url = app.state.OLLAMA_BASE_URLS[url_idx] - print(url) + log.info(f"url: {url}") r = None def get_request(): nonlocal url nonlocal r + + request_id = str(uuid.uuid4()) try: + REQUEST_POOL.append(request_id) def stream_content(): - for chunk in r.iter_content(chunk_size=8192): - yield chunk + try: + yield json.dumps({"id": request_id, "done": False}) + "\n" + + for chunk in r.iter_content(chunk_size=8192): + if request_id in REQUEST_POOL: + yield chunk + else: + print("User: canceled request") + break + finally: + if hasattr(r, "close"): + r.close() + if request_id in REQUEST_POOL: + REQUEST_POOL.remove(request_id) r = requests.request( method="POST", @@ -275,8 +293,9 @@ async def pull_model( try: return await run_in_threadpool(get_request) + except Exception as e: - print(e) + log.exception(e) error_detail = "Open WebUI: Server Connection Error" if r is not None: try: @@ -315,7 +334,7 @@ async def push_model( ) url = app.state.OLLAMA_BASE_URLS[url_idx] - print(url) + log.debug(f"url: {url}") r = None @@ -347,7 +366,7 @@ async def push_model( try: return await run_in_threadpool(get_request) except Exception as e: - print(e) + log.exception(e) error_detail = "Open WebUI: Server Connection Error" if r is not None: try: @@ -375,9 +394,9 @@ class CreateModelForm(BaseModel): async def create_model( form_data: CreateModelForm, url_idx: int = 0, user=Depends(get_admin_user) ): - print(form_data) + log.debug(f"form_data: {form_data}") url = app.state.OLLAMA_BASE_URLS[url_idx] - print(url) + log.info(f"url: {url}") r = None @@ -399,7 +418,7 @@ async def create_model( r.raise_for_status() - print(r) + log.debug(f"r: {r}") return StreamingResponse( stream_content(), @@ -412,7 +431,7 @@ async def create_model( try: return await run_in_threadpool(get_request) except Exception as e: - print(e) + log.exception(e) error_detail = "Open WebUI: Server Connection Error" if r is not None: try: @@ -450,7 +469,7 @@ async def copy_model( ) url = app.state.OLLAMA_BASE_URLS[url_idx] - print(url) + log.info(f"url: {url}") try: r = requests.request( @@ -460,11 +479,11 @@ async def copy_model( ) r.raise_for_status() - print(r.text) + log.debug(f"r.text: {r.text}") return True except Exception as e: - print(e) + log.exception(e) error_detail = "Open WebUI: Server Connection Error" if r is not None: try: @@ -497,7 +516,7 @@ async def delete_model( ) url = app.state.OLLAMA_BASE_URLS[url_idx] - print(url) + log.info(f"url: {url}") try: r = requests.request( @@ -507,11 +526,11 @@ async def delete_model( ) r.raise_for_status() - print(r.text) + log.debug(f"r.text: {r.text}") return True except Exception as e: - print(e) + log.exception(e) error_detail = "Open WebUI: Server Connection Error" if r is not None: try: @@ -537,7 +556,7 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_current_use url_idx = random.choice(app.state.MODELS[form_data.name]["urls"]) url = app.state.OLLAMA_BASE_URLS[url_idx] - print(url) + log.info(f"url: {url}") try: r = requests.request( @@ -549,7 +568,7 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_current_use return r.json() except Exception as e: - print(e) + log.exception(e) error_detail = "Open WebUI: Server Connection Error" if r is not None: try: @@ -589,7 +608,7 @@ async def generate_embeddings( ) url = app.state.OLLAMA_BASE_URLS[url_idx] - print(url) + log.info(f"url: {url}") try: r = requests.request( @@ -601,7 +620,7 @@ async def generate_embeddings( return r.json() except Exception as e: - print(e) + log.exception(e) error_detail = "Open WebUI: Server Connection Error" if r is not None: try: @@ -649,7 +668,7 @@ async def generate_completion( ) url = app.state.OLLAMA_BASE_URLS[url_idx] - print(url) + log.info(f"url: {url}") r = None @@ -670,7 +689,7 @@ async def generate_completion( if request_id in REQUEST_POOL: yield chunk else: - print("User: canceled request") + log.warning("User: canceled request") break finally: if hasattr(r, "close"): @@ -747,11 +766,11 @@ async def generate_chat_completion( ) url = app.state.OLLAMA_BASE_URLS[url_idx] - print(url) + log.info(f"url: {url}") r = None - print(form_data.model_dump_json(exclude_none=True).encode()) + log.debug("form_data.model_dump_json(exclude_none=True).encode(): {0} ".format(form_data.model_dump_json(exclude_none=True).encode())) def get_request(): nonlocal form_data @@ -770,7 +789,7 @@ async def generate_chat_completion( if request_id in REQUEST_POOL: yield chunk else: - print("User: canceled request") + log.warning("User: canceled request") break finally: if hasattr(r, "close"): @@ -793,7 +812,7 @@ async def generate_chat_completion( headers=dict(r.headers), ) except Exception as e: - print(e) + log.exception(e) raise e try: @@ -847,7 +866,7 @@ async def generate_openai_chat_completion( ) url = app.state.OLLAMA_BASE_URLS[url_idx] - print(url) + log.info(f"url: {url}") r = None @@ -870,7 +889,7 @@ async def generate_openai_chat_completion( if request_id in REQUEST_POOL: yield chunk else: - print("User: canceled request") + log.warning("User: canceled request") break finally: if hasattr(r, "close"): @@ -1168,7 +1187,7 @@ async def deprecated_proxy(path: str, request: Request, user=Depends(get_current if request_id in REQUEST_POOL: yield chunk else: - print("User: canceled request") + log.warning("User: canceled request") break finally: if hasattr(r, "close"): diff --git a/backend/apps/openai/main.py b/backend/apps/openai/main.py index 67a99794..4098d73a 100644 --- a/backend/apps/openai/main.py +++ b/backend/apps/openai/main.py @@ -6,6 +6,7 @@ import requests import aiohttp import asyncio import json +import logging from pydantic import BaseModel @@ -19,6 +20,7 @@ from utils.utils import ( get_admin_user, ) from config import ( + SRC_LOG_LEVELS, OPENAI_API_BASE_URLS, OPENAI_API_KEYS, CACHE_DIR, @@ -31,6 +33,9 @@ from typing import List, Optional import hashlib from pathlib import Path +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["OPENAI"]) + app = FastAPI() app.add_middleware( CORSMiddleware, @@ -134,7 +139,7 @@ async def speech(request: Request, user=Depends(get_verified_user)): return FileResponse(file_path) except Exception as e: - print(e) + log.exception(e) error_detail = "Open WebUI: Server Connection Error" if r is not None: try: @@ -160,7 +165,7 @@ async def fetch_url(url, key): return await response.json() except Exception as e: # Handle connection error here - print(f"Connection error: {e}") + log.error(f"Connection error: {e}") return None @@ -182,7 +187,7 @@ def merge_models_lists(model_lists): async def get_all_models(): - print("get_all_models") + log.info("get_all_models()") if len(app.state.OPENAI_API_KEYS) == 1 and app.state.OPENAI_API_KEYS[0] == "": models = {"data": []} @@ -208,7 +213,7 @@ async def get_all_models(): ) } - print(models) + log.info(f"models: {models}") app.state.MODELS = {model["id"]: model for model in models["data"]} return models @@ -246,7 +251,7 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_use return response_data except Exception as e: - print(e) + log.exception(e) error_detail = "Open WebUI: Server Connection Error" if r is not None: try: @@ -280,7 +285,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): if body.get("model") == "gpt-4-vision-preview": if "max_tokens" not in body: body["max_tokens"] = 4000 - print("Modified body_dict:", body) + log.debug("Modified body_dict:", body) # Fix for ChatGPT calls failing because the num_ctx key is in body if "num_ctx" in body: @@ -292,7 +297,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): # Convert the modified body back to JSON body = json.dumps(body) except json.JSONDecodeError as e: - print("Error loading request body into a dictionary:", e) + log.error("Error loading request body into a dictionary:", e) url = app.state.OPENAI_API_BASE_URLS[idx] key = app.state.OPENAI_API_KEYS[idx] @@ -330,7 +335,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): response_data = r.json() return response_data except Exception as e: - print(e) + log.exception(e) error_detail = "Open WebUI: Server Connection Error" if r is not None: try: diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py index 5fc38b4a..da7bb307 100644 --- a/backend/apps/rag/main.py +++ b/backend/apps/rag/main.py @@ -8,7 +8,7 @@ from fastapi import ( Form, ) from fastapi.middleware.cors import CORSMiddleware -import os, shutil +import os, shutil, logging from pathlib import Path from typing import List @@ -21,6 +21,7 @@ from langchain_community.document_loaders import ( TextLoader, PyPDFLoader, CSVLoader, + BSHTMLLoader, Docx2txtLoader, UnstructuredEPubLoader, UnstructuredWordDocumentLoader, @@ -54,6 +55,7 @@ from utils.misc import ( ) from utils.utils import get_current_user, get_admin_user from config import ( + SRC_LOG_LEVELS, UPLOAD_DIR, DOCS_DIR, RAG_EMBEDDING_MODEL, @@ -66,6 +68,9 @@ from config import ( from constants import ERROR_MESSAGES +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["RAG"]) + # # if RAG_EMBEDDING_MODEL: # sentence_transformer_ef = SentenceTransformer( @@ -111,39 +116,6 @@ class StoreWebForm(CollectionNameForm): url: str -def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool: - text_splitter = RecursiveCharacterTextSplitter( - chunk_size=app.state.CHUNK_SIZE, chunk_overlap=app.state.CHUNK_OVERLAP - ) - docs = text_splitter.split_documents(data) - - texts = [doc.page_content for doc in docs] - metadatas = [doc.metadata for doc in docs] - - try: - if overwrite: - for collection in CHROMA_CLIENT.list_collections(): - if collection_name == collection.name: - print(f"deleting existing collection {collection_name}") - CHROMA_CLIENT.delete_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] - ) - return True - except Exception as e: - print(e) - if e.__class__.__name__ == "UniqueConstraintError": - return True - - return False - - @app.get("/") async def get_status(): return { @@ -274,7 +246,7 @@ def query_doc_handler( embedding_function=app.state.sentence_transformer_ef, ) except Exception as e: - print(e) + log.exception(e) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e), @@ -318,13 +290,69 @@ def store_web(form_data: StoreWebForm, user=Depends(get_current_user)): "filename": form_data.url, } except Exception as e: - print(e) + log.exception(e) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e), ) +def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool: + + text_splitter = RecursiveCharacterTextSplitter( + chunk_size=app.state.CHUNK_SIZE, + chunk_overlap=app.state.CHUNK_OVERLAP, + add_start_index=True, + ) + docs = text_splitter.split_documents(data) + + if len(docs) > 0: + return store_docs_in_vector_db(docs, collection_name, overwrite), None + else: + raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT) + + +def store_text_in_vector_db( + text, metadata, collection_name, overwrite: bool = False +) -> bool: + text_splitter = RecursiveCharacterTextSplitter( + chunk_size=app.state.CHUNK_SIZE, + chunk_overlap=app.state.CHUNK_OVERLAP, + add_start_index=True, + ) + docs = text_splitter.create_documents([text], metadatas=[metadata]) + return store_docs_in_vector_db(docs, collection_name, overwrite) + + +def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> bool: + + texts = [doc.page_content for doc in docs] + metadatas = [doc.metadata for doc in docs] + + try: + if overwrite: + for collection in CHROMA_CLIENT.list_collections(): + if collection_name == collection.name: + print(f"deleting existing collection {collection_name}") + CHROMA_CLIENT.delete_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] + ) + return True + except Exception as e: + print(e) + if e.__class__.__name__ == "UniqueConstraintError": + return True + + return False + + def get_loader(filename: str, file_content_type: str, file_path: str): file_ext = filename.split(".")[-1].lower() known_type = True @@ -382,6 +410,8 @@ def get_loader(filename: str, file_content_type: str, file_path: str): loader = UnstructuredRSTLoader(file_path, mode="elements") elif file_ext == "xml": loader = UnstructuredXMLLoader(file_path) + elif file_ext in ["htm", "html"]: + loader = BSHTMLLoader(file_path, open_encoding="unicode_escape") elif file_ext == "md": loader = UnstructuredMarkdownLoader(file_path) elif file_content_type == "application/epub+zip": @@ -416,7 +446,7 @@ def store_doc( ): # "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm" - print(file.content_type) + log.info(f"file.content_type: {file.content_type}") try: filename = file.filename file_path = f"{UPLOAD_DIR}/{filename}" @@ -432,22 +462,24 @@ def store_doc( loader, known_type = get_loader(file.filename, file.content_type, file_path) data = loader.load() - result = store_data_in_vector_db(data, collection_name) - if result: - return { - "status": True, - "collection_name": collection_name, - "filename": filename, - "known_type": known_type, - } - else: + try: + result = store_data_in_vector_db(data, collection_name) + + if result: + return { + "status": True, + "collection_name": collection_name, + "filename": filename, + "known_type": known_type, + } + except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=ERROR_MESSAGES.DEFAULT(), + detail=e, ) except Exception as e: - print(e) + log.exception(e) if "No pandoc was found" in str(e): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -460,6 +492,37 @@ def store_doc( ) +class TextRAGForm(BaseModel): + name: str + content: str + collection_name: Optional[str] = None + + +@app.post("/text") +def store_text( + form_data: TextRAGForm, + user=Depends(get_current_user), +): + + collection_name = form_data.collection_name + if collection_name == None: + collection_name = calculate_sha256_string(form_data.content) + + result = store_text_in_vector_db( + form_data.content, + metadata={"name": form_data.name, "created_by": user.id}, + collection_name=collection_name, + ) + + if result: + return {"status": True, "collection_name": collection_name} + else: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + @app.get("/scan") def scan_docs_dir(user=Depends(get_admin_user)): for path in Path(DOCS_DIR).rglob("./**/*"): @@ -478,41 +541,45 @@ def scan_docs_dir(user=Depends(get_admin_user)): ) data = loader.load() - result = store_data_in_vector_db(data, collection_name) + try: + result = store_data_in_vector_db(data, collection_name) - if result: - sanitized_filename = sanitize_filename(filename) - doc = Documents.get_doc_by_name(sanitized_filename) + if result: + sanitized_filename = sanitize_filename(filename) + doc = Documents.get_doc_by_name(sanitized_filename) - if doc == None: - doc = Documents.insert_new_doc( - user.id, - DocumentForm( - **{ - "name": sanitized_filename, - "title": filename, - "collection_name": collection_name, - "filename": filename, - "content": ( - json.dumps( - { - "tags": list( - map( - lambda name: {"name": name}, - tags, + if doc == None: + doc = Documents.insert_new_doc( + user.id, + DocumentForm( + **{ + "name": sanitized_filename, + "title": filename, + "collection_name": collection_name, + "filename": filename, + "content": ( + json.dumps( + { + "tags": list( + map( + lambda name: {"name": name}, + tags, + ) ) - ) - } - ) - if len(tags) - else "{}" - ), - } - ), - ) + } + ) + if len(tags) + else "{}" + ), + } + ), + ) + except Exception as e: + print(e) + pass except Exception as e: - print(e) + log.exception(e) return True @@ -533,11 +600,11 @@ def reset(user=Depends(get_admin_user)) -> bool: elif os.path.isdir(file_path): shutil.rmtree(file_path) except Exception as e: - print("Failed to delete %s. Reason: %s" % (file_path, e)) + log.error("Failed to delete %s. Reason: %s" % (file_path, e)) try: CHROMA_CLIENT.reset() except Exception as e: - print(e) + log.exception(e) return True diff --git a/backend/apps/rag/utils.py b/backend/apps/rag/utils.py index a3537d4d..ccdc03b4 100644 --- a/backend/apps/rag/utils.py +++ b/backend/apps/rag/utils.py @@ -1,7 +1,11 @@ import re +import logging from typing import List -from config import CHROMA_CLIENT +from config import SRC_LOG_LEVELS, CHROMA_CLIENT + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["RAG"]) def query_doc(collection_name: str, query: str, k: int, embedding_function): @@ -97,7 +101,7 @@ def rag_template(template: str, context: str, query: str): def rag_messages(docs, messages, template, k, embedding_function): - print(docs) + log.debug(f"docs: {docs}") last_user_message_idx = None for i in range(len(messages) - 1, -1, -1): @@ -137,6 +141,8 @@ def rag_messages(docs, messages, template, k, embedding_function): k=k, embedding_function=embedding_function, ) + elif doc["type"] == "text": + context = doc["content"] else: context = query_doc( collection_name=doc["collection_name"], @@ -145,7 +151,7 @@ def rag_messages(docs, messages, template, k, embedding_function): embedding_function=embedding_function, ) except Exception as e: - print(e) + log.exception(e) context = None relevant_contexts.append(context) diff --git a/backend/apps/web/internal/db.py b/backend/apps/web/internal/db.py index d0aa9969..554f8002 100644 --- a/backend/apps/web/internal/db.py +++ b/backend/apps/web/internal/db.py @@ -1,13 +1,16 @@ from peewee import * -from config import DATA_DIR +from config import SRC_LOG_LEVELS, DATA_DIR import os +import logging +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["DB"]) # Check if the file exists if os.path.exists(f"{DATA_DIR}/ollama.db"): # Rename the file os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db") - print("File renamed successfully.") + log.info("File renamed successfully.") else: pass diff --git a/backend/apps/web/models/auths.py b/backend/apps/web/models/auths.py index 02d2ab86..b26236ef 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 +import logging from peewee import * from apps.web.models.users import UserModel, Users @@ -9,6 +10,10 @@ from utils.utils import verify_password from apps.web.internal.db import DB +from config import SRC_LOG_LEVELS +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + #################### # DB MODEL #################### @@ -86,7 +91,7 @@ class AuthsTable: def insert_new_auth( self, email: str, password: str, name: str, role: str = "pending" ) -> Optional[UserModel]: - print("insert_new_auth") + log.info("insert_new_auth") id = str(uuid.uuid4()) @@ -103,7 +108,7 @@ class AuthsTable: return None def authenticate_user(self, email: str, password: str) -> Optional[UserModel]: - print("authenticate_user", email) + log.info(f"authenticate_user: {email}") try: auth = Auth.get(Auth.email == email, Auth.active == True) if auth: diff --git a/backend/apps/web/models/documents.py b/backend/apps/web/models/documents.py index 6a372b2c..f399e7ae 100644 --- a/backend/apps/web/models/documents.py +++ b/backend/apps/web/models/documents.py @@ -3,6 +3,7 @@ from peewee import * from playhouse.shortcuts import model_to_dict from typing import List, Union, Optional import time +import logging from utils.utils import decode_token from utils.misc import get_gravatar_url @@ -11,6 +12,10 @@ from apps.web.internal.db import DB import json +from config import SRC_LOG_LEVELS +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + #################### # Documents DB Schema #################### @@ -118,7 +123,7 @@ class DocumentsTable: doc = Document.get(Document.name == form_data.name) return DocumentModel(**model_to_dict(doc)) except Exception as e: - print(e) + log.exception(e) return None def update_doc_content_by_name( @@ -138,7 +143,7 @@ class DocumentsTable: doc = Document.get(Document.name == name) return DocumentModel(**model_to_dict(doc)) except Exception as e: - print(e) + log.exception(e) return None def delete_doc_by_name(self, name: str) -> bool: diff --git a/backend/apps/web/models/tags.py b/backend/apps/web/models/tags.py index d4264501..476e6693 100644 --- a/backend/apps/web/models/tags.py +++ b/backend/apps/web/models/tags.py @@ -6,9 +6,14 @@ from playhouse.shortcuts import model_to_dict import json import uuid import time +import logging from apps.web.internal.db import DB +from config import SRC_LOG_LEVELS +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + #################### # Tag DB Schema #################### @@ -173,7 +178,7 @@ class TagTable: (ChatIdTag.tag_name == tag_name) & (ChatIdTag.user_id == user_id) ) res = query.execute() # Remove the rows, return number of rows removed. - print(res) + log.debug(f"res: {res}") tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id) if tag_count == 0: @@ -185,7 +190,7 @@ class TagTable: return True except Exception as e: - print("delete_tag", e) + log.error(f"delete_tag: {e}") return False def delete_tag_by_tag_name_and_chat_id_and_user_id( @@ -198,7 +203,7 @@ class TagTable: & (ChatIdTag.user_id == user_id) ) res = query.execute() # Remove the rows, return number of rows removed. - print(res) + log.debug(f"res: {res}") tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id) if tag_count == 0: @@ -210,7 +215,7 @@ class TagTable: return True except Exception as e: - print("delete_tag", e) + log.error(f"delete_tag: {e}") return False def delete_tags_by_chat_id_and_user_id(self, chat_id: str, user_id: str) -> bool: diff --git a/backend/apps/web/routers/chats.py b/backend/apps/web/routers/chats.py index 0c0ac1ce..d018b31b 100644 --- a/backend/apps/web/routers/chats.py +++ b/backend/apps/web/routers/chats.py @@ -5,6 +5,7 @@ from utils.utils import get_current_user, get_admin_user from fastapi import APIRouter from pydantic import BaseModel import json +import logging from apps.web.models.users import Users from apps.web.models.chats import ( @@ -27,6 +28,10 @@ from apps.web.models.tags import ( from constants import ERROR_MESSAGES +from config import SRC_LOG_LEVELS +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + router = APIRouter() ############################ @@ -78,7 +83,7 @@ async def create_new_chat(form_data: ChatForm, user=Depends(get_current_user)): chat = Chats.insert_new_chat(user.id, form_data) return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) except Exception as e: - print(e) + log.exception(e) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() ) @@ -95,7 +100,7 @@ async def get_all_tags(user=Depends(get_current_user)): tags = Tags.get_tags_by_user_id(user.id) return tags except Exception as e: - print(e) + log.exception(e) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() ) diff --git a/backend/apps/web/routers/users.py b/backend/apps/web/routers/users.py index b8e2732c..67c136da 100644 --- a/backend/apps/web/routers/users.py +++ b/backend/apps/web/routers/users.py @@ -7,6 +7,7 @@ from fastapi import APIRouter from pydantic import BaseModel import time import uuid +import logging from apps.web.models.users import UserModel, UserUpdateForm, UserRoleUpdateForm, Users from apps.web.models.auths import Auths @@ -14,6 +15,10 @@ from apps.web.models.auths import Auths from utils.utils import get_current_user, get_password_hash, get_admin_user from constants import ERROR_MESSAGES +from config import SRC_LOG_LEVELS +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + router = APIRouter() ############################ @@ -83,7 +88,7 @@ async def update_user_by_id( if form_data.password: hashed = get_password_hash(form_data.password) - print(hashed) + log.debug(f"hashed: {hashed}") Auths.update_user_password_by_id(user_id, hashed) Auths.update_email_by_id(user_id, form_data.email.lower()) diff --git a/backend/config.py b/backend/config.py index 613b747a..27311fac 100644 --- a/backend/config.py +++ b/backend/config.py @@ -1,24 +1,29 @@ -import json import os -import shutil -from base64 import b64encode -from pathlib import Path -from secrets import token_bytes - +import sys +import logging import chromadb +from chromadb import Settings +from base64 import b64encode +from bs4 import BeautifulSoup + +from pathlib import Path +import json +import yaml + import markdown import requests -import yaml -from bs4 import BeautifulSoup -from chromadb import Settings +import shutil + +from secrets import token_bytes from constants import ERROR_MESSAGES + try: - from dotenv import find_dotenv, load_dotenv + from dotenv import load_dotenv, find_dotenv load_dotenv(find_dotenv("../.env")) except ImportError: - print("dotenv not installed, skipping...") + log.warning("dotenv not installed, skipping...") WEBUI_NAME = "Open WebUI" WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png" @@ -98,6 +103,34 @@ for version in soup.find_all("h2"): CHANGELOG = changelog_json +#################################### +# LOGGING +#################################### +log_levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"] + +GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper() +if GLOBAL_LOG_LEVEL in log_levels: + logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True) +else: + GLOBAL_LOG_LEVEL = "INFO" + +log = logging.getLogger(__name__) +log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}") + +log_sources = ["AUDIO", "CONFIG", "DB", "IMAGES", "LITELLM", "MAIN", "MODELS", "OLLAMA", "OPENAI", "RAG"] + +SRC_LOG_LEVELS = {} + +for source in log_sources: + log_env_var = source + "_LOG_LEVEL" + SRC_LOG_LEVELS[source] = os.environ.get(log_env_var, "").upper() + if SRC_LOG_LEVELS[source] not in log_levels: + SRC_LOG_LEVELS[source] = GLOBAL_LOG_LEVEL + log.info(f"{log_env_var}: {SRC_LOG_LEVELS[source]}") + +log.setLevel(SRC_LOG_LEVELS["CONFIG"]) + + #################################### # CUSTOM_NAME #################################### @@ -123,7 +156,7 @@ if CUSTOM_NAME: WEBUI_NAME = data["name"] except Exception as e: - print(e) + log.exception(e) pass @@ -192,9 +225,9 @@ def create_config_file(file_path): LITELLM_CONFIG_PATH = f"{DATA_DIR}/litellm/config.yaml" if not os.path.exists(LITELLM_CONFIG_PATH): - print("Config file doesn't exist. Creating...") + log.info("Config file doesn't exist. Creating...") create_config_file(LITELLM_CONFIG_PATH) - print("Config file created successfully.") + log.info("Config file created successfully.") #################################### @@ -206,7 +239,7 @@ OLLAMA_API_BASE_URL = os.environ.get( ) OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "") - +KUBERNETES_SERVICE_HOST = os.environ.get("KUBERNETES_SERVICE_HOST", "") if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "": OLLAMA_BASE_URL = ( @@ -216,8 +249,10 @@ if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "": ) if ENV == "prod": - if OLLAMA_BASE_URL == "/ollama": + if OLLAMA_BASE_URL == "/ollama" and KUBERNETES_SERVICE_HOST == "": OLLAMA_BASE_URL = "http://host.docker.internal:11434" + else: + OLLAMA_BASE_URL = "http://ollama-service.open-webui.svc.cluster.local:11434" OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "") diff --git a/backend/constants.py b/backend/constants.py index 42c5c85e..8bcdd078 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -60,3 +60,5 @@ class ERROR_MESSAGES(str, Enum): MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found" OPENAI_NOT_FOUND = lambda name="": f"OpenAI API was not found" OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama" + + EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding." diff --git a/backend/main.py b/backend/main.py index 35f405e6..d4b67079 100644 --- a/backend/main.py +++ b/backend/main.py @@ -4,6 +4,7 @@ import markdown import time import os import sys +import logging import requests from fastapi import FastAPI, Request, Depends, status @@ -38,10 +39,15 @@ from config import ( FRONTEND_BUILD_DIR, MODEL_FILTER_ENABLED, MODEL_FILTER_LIST, + GLOBAL_LOG_LEVEL, + SRC_LOG_LEVELS, WEBHOOK_URL, ) from constants import ERROR_MESSAGES +logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MAIN"]) class SPAStaticFiles(StaticFiles): async def get_response(self, path: str, scope): @@ -70,7 +76,7 @@ class RAGMiddleware(BaseHTTPMiddleware): if request.method == "POST" and ( "/api/chat" in request.url.path or "/chat/completions" in request.url.path ): - print(request.url.path) + log.debug(f"request.url.path: {request.url.path}") # Read the original request body body = await request.body() @@ -93,7 +99,7 @@ class RAGMiddleware(BaseHTTPMiddleware): ) del data["docs"] - print(data["messages"]) + log.debug(f"data['messages']: {data['messages']}") modified_body_bytes = json.dumps(data).encode("utf-8") diff --git a/kubernetes/manifest/base/webui-deployment.yaml b/kubernetes/manifest/base/webui-deployment.yaml index 38efd554..79a0a9a2 100644 --- a/kubernetes/manifest/base/webui-deployment.yaml +++ b/kubernetes/manifest/base/webui-deployment.yaml @@ -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/manifest/base/webui-pvc.yaml b/kubernetes/manifest/base/webui-pvc.yaml index 5c75283a..97fb761d 100644 --- a/kubernetes/manifest/base/webui-pvc.yaml +++ b/kubernetes/manifest/base/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: open-webui spec: accessModes: ["ReadWriteOnce"] diff --git a/src/lib/apis/litellm/index.ts b/src/lib/apis/litellm/index.ts index 302e9c4a..643146b7 100644 --- a/src/lib/apis/litellm/index.ts +++ b/src/lib/apis/litellm/index.ts @@ -33,7 +33,7 @@ export const getLiteLLMModels = async (token: string = '') => { id: model.id, name: model.name ?? model.id, external: true, - source: 'litellm' + source: 'LiteLLM' })) .sort((a, b) => { return a.name.localeCompare(b.name); diff --git a/src/lib/apis/ollama/index.ts b/src/lib/apis/ollama/index.ts index c3b37e00..7c4e809e 100644 --- a/src/lib/apis/ollama/index.ts +++ b/src/lib/apis/ollama/index.ts @@ -271,7 +271,7 @@ export const generateChatCompletion = async (token: string = '', body: object) = return [res, controller]; }; -export const cancelChatCompletion = async (token: string = '', requestId: string) => { +export const cancelOllamaRequest = async (token: string = '', requestId: string) => { let error = null; const res = await fetch(`${OLLAMA_API_BASE_URL}/cancel/${requestId}`, { diff --git a/src/lib/apis/openai/index.ts b/src/lib/apis/openai/index.ts index e38314a5..7d0d9415 100644 --- a/src/lib/apis/openai/index.ts +++ b/src/lib/apis/openai/index.ts @@ -263,3 +263,53 @@ export const synthesizeOpenAISpeech = async ( return res; }; + +export const generateTitle = async ( + token: string = '', + template: string, + model: string, + prompt: string, + url: string = OPENAI_API_BASE_URL +) => { + let error = null; + + template = template.replace(/{{prompt}}/g, prompt); + + console.log(template); + + const res = await fetch(`${url}/chat/completions`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + messages: [ + { + role: 'user', + content: template + } + ], + stream: false + }) + }) + .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; + } + return null; + }); + + if (error) { + throw error; + } + + return res?.choices[0]?.message?.content ?? 'New Chat'; +}; diff --git a/src/lib/components/chat/ModelSelector.svelte b/src/lib/components/chat/ModelSelector.svelte index df78a1fe..b5292943 100644 --- a/src/lib/components/chat/ModelSelector.svelte +++ b/src/lib/components/chat/ModelSelector.svelte @@ -3,6 +3,7 @@ import { models, showSettings, settings, user } from '$lib/stores'; import { onMount, tick, getContext } from 'svelte'; import { toast } from 'svelte-sonner'; + import Selector from './ModelSelector/Selector.svelte'; const i18n = getContext('i18n'); @@ -32,30 +33,24 @@ } -