diff --git a/CHANGELOG.md b/CHANGELOG.md index a3bceff25..d62360f87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,37 @@ 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.3.11] - 2024-08-02 + +### Added + +- **📊 Model Information Display**: Added visuals for model selection, including images next to model names for more intuitive navigation. +- **🗣 ElevenLabs Voice Adaptations**: Voice enhancements including support for ElevenLabs voice ID by name for personalized vocal interactions. +- **⌨️ Arrow Keys Model Selection**: Users can now use arrow keys for quicker model selection, enhancing accessibility. +- **🔍 Fuzzy Search in Model Selector**: Enhanced model selector with fuzzy search to locate models swiftly, including descriptions. +- **🕹️ ComfyUI Flux Image Generation**: Added support for the new Flux image gen model; introduces environment controls like weight precision and CLIP model options in Settings. +- **💾 Display File Size for Uploads**: Enhanced file interface now displays file size, preparing for upcoming upload restrictions. +- **🎚️ Advanced Params "Min P"**: Added 'Min P' parameter in the advanced settings for customized model precision control. +- **🔒 Enhanced OAuth**: Introduced custom redirect URI support for OAuth behind reverse proxies, enabling safer authentication processes. +- **🖥 Enhanced Latex Rendering**: Adjustments made to latex rendering processes, now accurately detecting and presenting latex inputs from text. +- **🌐 Internationalization**: Enhanced with new Romanian and updated Vietnamese and Ukrainian translations, helping broaden accessibility for international users. + +### Fixed + +- **🔧 Tags Handling in Document Upload**: Tags are now properly sent to the upload document handler, resolving issues with missing metadata. +- **🖥️ Sensitive Input Fields**: Corrected browser misinterpretation of secure input fields, preventing misclassification as password fields. +- **📂 Static Path Resolution in PDF Generation**: Fixed static paths that adjust dynamically to prevent issues across various environments. + +### Changed + +- **🎨 UI/UX Styling Enhancements**: Multiple minor styling updates for a cleaner and more intuitive user interface. +- **🚧 Refactoring Various Components**: Numerous refactoring changes across styling, file handling, and function simplifications for clarity and performance. +- **🎛️ User Valves Management**: Moved user valves from settings to direct chat controls for more user-friendly access during interactions. + +### Removed + +- **⚙️ Health Check Logging**: Removed verbose logging from the health checking processes to declutter logs and improve backend performance. + ## [0.3.10] - 2024-07-17 ### Fixed diff --git a/Dockerfile b/Dockerfile index a217595ea..8078bf0ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -151,7 +151,7 @@ COPY --chown=$UID:$GID ./backend . EXPOSE 8080 -HEALTHCHECK CMD curl --silent --fail http://localhost:8080/health | jq -e '.status == true' || exit 1 +HEALTHCHECK CMD curl --silent --fail http://localhost:${PORT:-8080}/health | jq -ne 'input.status == true' || exit 1 USER $UID:$GID diff --git a/backend/apps/audio/main.py b/backend/apps/audio/main.py index c565bf481..167db77ba 100644 --- a/backend/apps/audio/main.py +++ b/backend/apps/audio/main.py @@ -10,12 +10,12 @@ from fastapi import ( File, Form, ) - from fastapi.responses import StreamingResponse, JSONResponse, FileResponse from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel +from typing import List import uuid import requests import hashlib @@ -31,6 +31,7 @@ from utils.utils import ( ) from utils.misc import calculate_sha256 + from config import ( SRC_LOG_LEVELS, CACHE_DIR, @@ -252,15 +253,15 @@ async def speech(request: Request, user=Depends(get_verified_user)): ) elif app.state.config.TTS_ENGINE == "elevenlabs": - payload = None try: payload = json.loads(body.decode("utf-8")) except Exception as e: log.exception(e) - pass + raise HTTPException(status_code=400, detail="Invalid JSON payload") - url = f"https://api.elevenlabs.io/v1/text-to-speech/{payload['voice']}" + voice_id = payload.get("voice", "") + url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}" headers = { "Accept": "audio/mpeg", @@ -435,3 +436,69 @@ def transcribe( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e), ) + + +def get_available_models() -> List[dict]: + if app.state.config.TTS_ENGINE == "openai": + return [{"id": "tts-1"}, {"id": "tts-1-hd"}] + elif app.state.config.TTS_ENGINE == "elevenlabs": + headers = { + "xi-api-key": app.state.config.TTS_API_KEY, + "Content-Type": "application/json", + } + + try: + response = requests.get( + "https://api.elevenlabs.io/v1/models", headers=headers + ) + response.raise_for_status() + models = response.json() + return [ + {"name": model["name"], "id": model["model_id"]} for model in models + ] + except requests.RequestException as e: + log.error(f"Error fetching voices: {str(e)}") + return [] + + +@app.get("/models") +async def get_models(user=Depends(get_verified_user)): + return {"models": get_available_models()} + + +def get_available_voices() -> List[dict]: + if app.state.config.TTS_ENGINE == "openai": + return [ + {"name": "alloy", "id": "alloy"}, + {"name": "echo", "id": "echo"}, + {"name": "fable", "id": "fable"}, + {"name": "onyx", "id": "onyx"}, + {"name": "nova", "id": "nova"}, + {"name": "shimmer", "id": "shimmer"}, + ] + elif app.state.config.TTS_ENGINE == "elevenlabs": + headers = { + "xi-api-key": app.state.config.TTS_API_KEY, + "Content-Type": "application/json", + } + + try: + response = requests.get( + "https://api.elevenlabs.io/v1/voices", headers=headers + ) + response.raise_for_status() + voices_data = response.json() + + voices = [] + for voice in voices_data.get("voices", []): + voices.append({"name": voice["name"], "id": voice["voice_id"]}) + return voices + except requests.RequestException as e: + log.error(f"Error fetching voices: {str(e)}") + + return [] + + +@app.get("/voices") +async def get_voices(user=Depends(get_verified_user)): + return {"voices": get_available_voices()} diff --git a/backend/apps/images/main.py b/backend/apps/images/main.py index 9ae0ad67b..4239f3f45 100644 --- a/backend/apps/images/main.py +++ b/backend/apps/images/main.py @@ -42,6 +42,9 @@ from config import ( COMFYUI_SAMPLER, COMFYUI_SCHEDULER, COMFYUI_SD3, + COMFYUI_FLUX, + COMFYUI_FLUX_WEIGHT_DTYPE, + COMFYUI_FLUX_FP8_CLIP, IMAGES_OPENAI_API_BASE_URL, IMAGES_OPENAI_API_KEY, IMAGE_GENERATION_MODEL, @@ -85,6 +88,9 @@ app.state.config.COMFYUI_CFG_SCALE = COMFYUI_CFG_SCALE app.state.config.COMFYUI_SAMPLER = COMFYUI_SAMPLER app.state.config.COMFYUI_SCHEDULER = COMFYUI_SCHEDULER app.state.config.COMFYUI_SD3 = COMFYUI_SD3 +app.state.config.COMFYUI_FLUX = COMFYUI_FLUX +app.state.config.COMFYUI_FLUX_WEIGHT_DTYPE = COMFYUI_FLUX_WEIGHT_DTYPE +app.state.config.COMFYUI_FLUX_FP8_CLIP = COMFYUI_FLUX_FP8_CLIP def get_automatic1111_api_auth(): @@ -497,6 +503,15 @@ async def image_generations( if app.state.config.COMFYUI_SD3 is not None: data["sd3"] = app.state.config.COMFYUI_SD3 + if app.state.config.COMFYUI_FLUX is not None: + data["flux"] = app.state.config.COMFYUI_FLUX + + if app.state.config.COMFYUI_FLUX_WEIGHT_DTYPE is not None: + data["flux_weight_dtype"] = app.state.config.COMFYUI_FLUX_WEIGHT_DTYPE + + if app.state.config.COMFYUI_FLUX_FP8_CLIP is not None: + data["flux_fp8_clip"] = app.state.config.COMFYUI_FLUX_FP8_CLIP + data = ImageGenerationPayload(**data) res = comfyui_generate_image( diff --git a/backend/apps/images/utils/comfyui.py b/backend/apps/images/utils/comfyui.py index 599b1f337..6c37f0c49 100644 --- a/backend/apps/images/utils/comfyui.py +++ b/backend/apps/images/utils/comfyui.py @@ -125,6 +125,135 @@ COMFYUI_DEFAULT_PROMPT = """ } """ +FLUX_DEFAULT_PROMPT = """ +{ + "5": { + "inputs": { + "width": 1024, + "height": 1024, + "batch_size": 1 + }, + "class_type": "EmptyLatentImage" + }, + "6": { + "inputs": { + "text": "Input Text Here", + "clip": [ + "11", + 0 + ] + }, + "class_type": "CLIPTextEncode" + }, + "8": { + "inputs": { + "samples": [ + "13", + 0 + ], + "vae": [ + "10", + 0 + ] + }, + "class_type": "VAEDecode" + }, + "9": { + "inputs": { + "filename_prefix": "ComfyUI", + "images": [ + "8", + 0 + ] + }, + "class_type": "SaveImage" + }, + "10": { + "inputs": { + "vae_name": "ae.sft" + }, + "class_type": "VAELoader" + }, + "11": { + "inputs": { + "clip_name1": "clip_l.safetensors", + "clip_name2": "t5xxl_fp16.safetensors", + "type": "flux" + }, + "class_type": "DualCLIPLoader" + }, + "12": { + "inputs": { + "unet_name": "flux1-dev.sft", + "weight_dtype": "default" + }, + "class_type": "UNETLoader" + }, + "13": { + "inputs": { + "noise": [ + "25", + 0 + ], + "guider": [ + "22", + 0 + ], + "sampler": [ + "16", + 0 + ], + "sigmas": [ + "17", + 0 + ], + "latent_image": [ + "5", + 0 + ] + }, + "class_type": "SamplerCustomAdvanced" + }, + "16": { + "inputs": { + "sampler_name": "euler" + }, + "class_type": "KSamplerSelect" + }, + "17": { + "inputs": { + "scheduler": "simple", + "steps": 20, + "denoise": 1, + "model": [ + "12", + 0 + ] + }, + "class_type": "BasicScheduler" + }, + "22": { + "inputs": { + "model": [ + "12", + 0 + ], + "conditioning": [ + "6", + 0 + ] + }, + "class_type": "BasicGuider" + }, + "25": { + "inputs": { + "noise_seed": 778937779713005 + }, + "class_type": "RandomNoise" + } +} +""" + def queue_prompt(prompt, client_id, base_url): log.info("queue_prompt") @@ -194,6 +323,9 @@ class ImageGenerationPayload(BaseModel): sampler: Optional[str] = None scheduler: Optional[str] = None sd3: Optional[bool] = None + flux: Optional[bool] = None + flux_weight_dtype: Optional[str] = None + flux_fp8_clip: Optional[bool] = None def comfyui_generate_image( @@ -215,21 +347,46 @@ def comfyui_generate_image( if payload.sd3: comfyui_prompt["5"]["class_type"] = "EmptySD3LatentImage" + if payload.steps: + comfyui_prompt["3"]["inputs"]["steps"] = payload.steps + comfyui_prompt["4"]["inputs"]["ckpt_name"] = model + comfyui_prompt["7"]["inputs"]["text"] = payload.negative_prompt + comfyui_prompt["3"]["inputs"]["seed"] = ( + payload.seed if payload.seed else random.randint(0, 18446744073709551614) + ) + + # as Flux uses a completely different workflow, we must treat it specially + if payload.flux: + comfyui_prompt = json.loads(FLUX_DEFAULT_PROMPT) + comfyui_prompt["12"]["inputs"]["unet_name"] = model + comfyui_prompt["25"]["inputs"]["noise_seed"] = ( + payload.seed if payload.seed else random.randint(0, 18446744073709551614) + ) + + if payload.sampler: + comfyui_prompt["16"]["inputs"]["sampler_name"] = payload.sampler + + if payload.steps: + comfyui_prompt["17"]["inputs"]["steps"] = payload.steps + + if payload.scheduler: + comfyui_prompt["17"]["inputs"]["scheduler"] = payload.scheduler + + if payload.flux_weight_dtype: + comfyui_prompt["12"]["inputs"]["weight_dtype"] = payload.flux_weight_dtype + + if payload.flux_fp8_clip: + comfyui_prompt["11"]["inputs"][ + "clip_name2" + ] = "t5xxl_fp8_e4m3fn.safetensors" + comfyui_prompt["5"]["inputs"]["batch_size"] = payload.n comfyui_prompt["5"]["inputs"]["width"] = payload.width comfyui_prompt["5"]["inputs"]["height"] = payload.height # set the text prompt for our positive CLIPTextEncode comfyui_prompt["6"]["inputs"]["text"] = payload.prompt - comfyui_prompt["7"]["inputs"]["text"] = payload.negative_prompt - - if payload.steps: - comfyui_prompt["3"]["inputs"]["steps"] = payload.steps - - comfyui_prompt["3"]["inputs"]["seed"] = ( - payload.seed if payload.seed else random.randint(0, 18446744073709551614) - ) try: ws = websocket.WebSocket() diff --git a/backend/apps/ollama/main.py b/backend/apps/ollama/main.py index 0e4d54e46..442d99ff2 100644 --- a/backend/apps/ollama/main.py +++ b/backend/apps/ollama/main.py @@ -857,6 +857,12 @@ async def generate_chat_completion( ): payload["options"]["top_p"] = model_info.params.get("top_p", None) + if ( + model_info.params.get("min_p", None) + and payload["options"].get("min_p") is None + ): + payload["options"]["min_p"] = model_info.params.get("min_p", None) + if ( model_info.params.get("use_mmap", None) and payload["options"].get("use_mmap") is None diff --git a/backend/apps/socket/main.py b/backend/apps/socket/main.py index 18ce7a607..1d98d37ff 100644 --- a/backend/apps/socket/main.py +++ b/backend/apps/socket/main.py @@ -52,7 +52,6 @@ async def user_join(sid, data): user = Users.get_user_by_id(data["id"]) if user: - SESSION_POOL[sid] = user.id if user.id in USER_POOL: USER_POOL[user.id].append(sid) @@ -80,7 +79,6 @@ def get_models_in_use(): @sio.on("usage") async def usage(sid, data): - model_id = data["model"] # Cancel previous callback if there is one @@ -139,7 +137,7 @@ async def disconnect(sid): print(f"Unknown session ID {sid} disconnected") -async def get_event_emitter(request_info): +def get_event_emitter(request_info): async def __event_emitter__(event_data): await sio.emit( "chat-events", @@ -154,7 +152,7 @@ async def get_event_emitter(request_info): return __event_emitter__ -async def get_event_call(request_info): +def get_event_call(request_info): async def __event_call__(event_data): response = await sio.call( "chat-events", diff --git a/backend/apps/webui/main.py b/backend/apps/webui/main.py index 97165a11b..972562a04 100644 --- a/backend/apps/webui/main.py +++ b/backend/apps/webui/main.py @@ -1,9 +1,6 @@ -from fastapi import FastAPI, Depends -from fastapi.routing import APIRoute +from fastapi import FastAPI from fastapi.responses import StreamingResponse from fastapi.middleware.cors import CORSMiddleware -from starlette.middleware.sessions import SessionMiddleware -from sqlalchemy.orm import Session from apps.webui.routers import ( auths, users, @@ -22,12 +19,15 @@ from apps.webui.models.functions import Functions from apps.webui.models.models import Models from apps.webui.utils import load_function_module_by_id -from utils.misc import stream_message_template +from utils.misc import ( + openai_chat_chunk_message_template, + openai_chat_completion_message_template, + add_or_update_system_message, +) from utils.task import prompt_template from config import ( - WEBUI_BUILD_HASH, SHOW_ADMIN_DETAILS, ADMIN_EMAIL, WEBUI_AUTH, @@ -51,11 +51,9 @@ from config import ( from apps.socket.main import get_event_call, get_event_emitter import inspect -import uuid -import time import json -from typing import Iterator, Generator, AsyncGenerator, Optional +from typing import Iterator, Generator, AsyncGenerator from pydantic import BaseModel app = FastAPI() @@ -127,60 +125,58 @@ async def get_status(): } +def get_function_module(pipe_id: str): + # Check if function is already loaded + if pipe_id not in app.state.FUNCTIONS: + function_module, _, _ = load_function_module_by_id(pipe_id) + app.state.FUNCTIONS[pipe_id] = function_module + else: + function_module = app.state.FUNCTIONS[pipe_id] + + if hasattr(function_module, "valves") and hasattr(function_module, "Valves"): + valves = Functions.get_function_valves_by_id(pipe_id) + function_module.valves = function_module.Valves(**(valves if valves else {})) + return function_module + + async def get_pipe_models(): pipes = Functions.get_functions_by_type("pipe", active_only=True) pipe_models = [] for pipe in pipes: - # Check if function is already loaded - if pipe.id not in app.state.FUNCTIONS: - function_module, function_type, frontmatter = load_function_module_by_id( - pipe.id - ) - app.state.FUNCTIONS[pipe.id] = function_module - else: - function_module = app.state.FUNCTIONS[pipe.id] - - if hasattr(function_module, "valves") and hasattr(function_module, "Valves"): - valves = Functions.get_function_valves_by_id(pipe.id) - function_module.valves = function_module.Valves( - **(valves if valves else {}) - ) + function_module = get_function_module(pipe.id) # Check if function is a manifold - if hasattr(function_module, "type"): - if function_module.type == "manifold": - manifold_pipes = [] + if hasattr(function_module, "pipes"): + manifold_pipes = [] - # Check if pipes is a function or a list - if callable(function_module.pipes): - manifold_pipes = function_module.pipes() - else: - manifold_pipes = function_module.pipes + # Check if pipes is a function or a list + if callable(function_module.pipes): + manifold_pipes = function_module.pipes() + else: + manifold_pipes = function_module.pipes - for p in manifold_pipes: - manifold_pipe_id = f'{pipe.id}.{p["id"]}' - manifold_pipe_name = p["name"] + for p in manifold_pipes: + manifold_pipe_id = f'{pipe.id}.{p["id"]}' + manifold_pipe_name = p["name"] - if hasattr(function_module, "name"): - manifold_pipe_name = ( - f"{function_module.name}{manifold_pipe_name}" - ) + if hasattr(function_module, "name"): + manifold_pipe_name = f"{function_module.name}{manifold_pipe_name}" - pipe_flag = {"type": pipe.type} - if hasattr(function_module, "ChatValves"): - pipe_flag["valves_spec"] = function_module.ChatValves.schema() + pipe_flag = {"type": pipe.type} + if hasattr(function_module, "ChatValves"): + pipe_flag["valves_spec"] = function_module.ChatValves.schema() - pipe_models.append( - { - "id": manifold_pipe_id, - "name": manifold_pipe_name, - "object": "model", - "created": pipe.created_at, - "owned_by": "openai", - "pipe": pipe_flag, - } - ) + pipe_models.append( + { + "id": manifold_pipe_id, + "name": manifold_pipe_name, + "object": "model", + "created": pipe.created_at, + "owned_by": "openai", + "pipe": pipe_flag, + } + ) else: pipe_flag = {"type": "pipe"} if hasattr(function_module, "ChatValves"): @@ -200,284 +196,211 @@ async def get_pipe_models(): return pipe_models +async def execute_pipe(pipe, params): + if inspect.iscoroutinefunction(pipe): + return await pipe(**params) + else: + return pipe(**params) + + +async def get_message_content(res: str | Generator | AsyncGenerator) -> str: + if isinstance(res, str): + return res + if isinstance(res, Generator): + return "".join(map(str, res)) + if isinstance(res, AsyncGenerator): + return "".join([str(stream) async for stream in res]) + + +def process_line(form_data: dict, line): + if isinstance(line, BaseModel): + line = line.model_dump_json() + line = f"data: {line}" + if isinstance(line, dict): + line = f"data: {json.dumps(line)}" + + try: + line = line.decode("utf-8") + except Exception: + pass + + if line.startswith("data:"): + return f"{line}\n\n" + else: + line = openai_chat_chunk_message_template(form_data["model"], line) + return f"data: {json.dumps(line)}\n\n" + + +def get_pipe_id(form_data: dict) -> str: + pipe_id = form_data["model"] + if "." in pipe_id: + pipe_id, _ = pipe_id.split(".", 1) + print(pipe_id) + return pipe_id + + +def get_function_params(function_module, form_data, user, extra_params={}): + pipe_id = get_pipe_id(form_data) + # Get the signature of the function + sig = inspect.signature(function_module.pipe) + params = {"body": form_data} + + for key, value in extra_params.items(): + if key in sig.parameters: + params[key] = value + + if "__user__" in sig.parameters: + __user__ = { + "id": user.id, + "email": user.email, + "name": user.name, + "role": user.role, + } + + try: + if hasattr(function_module, "UserValves"): + __user__["valves"] = function_module.UserValves( + **Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id) + ) + except Exception as e: + print(e) + + params["__user__"] = __user__ + return params + + +# inplace function: form_data is modified +def apply_model_params_to_body(params: dict, form_data: dict) -> dict: + if not params: + return form_data + + mappings = { + "temperature": float, + "top_p": int, + "max_tokens": int, + "frequency_penalty": int, + "seed": lambda x: x, + "stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x], + } + + for key, cast_func in mappings.items(): + if (value := params.get(key)) is not None: + form_data[key] = cast_func(value) + + return form_data + + +# inplace function: form_data is modified +def apply_model_system_prompt_to_body(params: dict, form_data: dict, user) -> dict: + system = params.get("system", None) + if not system: + return form_data + + if user: + template_params = { + "user_name": user.name, + "user_location": user.info.get("location") if user.info else None, + } + else: + template_params = {} + system = prompt_template(system, **template_params) + form_data["messages"] = add_or_update_system_message( + system, form_data.get("messages", []) + ) + return form_data + + async def generate_function_chat_completion(form_data, user): model_id = form_data.get("model") model_info = Models.get_model_by_id(model_id) - - metadata = None - if "metadata" in form_data: - metadata = form_data["metadata"] - del form_data["metadata"] + metadata = form_data.pop("metadata", None) __event_emitter__ = None __event_call__ = None __task__ = None if metadata: - if ( - metadata.get("session_id") - and metadata.get("chat_id") - and metadata.get("message_id") - ): - __event_emitter__ = await get_event_emitter(metadata) - __event_call__ = await get_event_call(metadata) - - if metadata.get("task"): - __task__ = metadata.get("task") + if all(k in metadata for k in ("session_id", "chat_id", "message_id")): + __event_emitter__ = get_event_emitter(metadata) + __event_call__ = get_event_call(metadata) + __task__ = metadata.get("task", None) if model_info: if model_info.base_model_id: form_data["model"] = model_info.base_model_id - model_info.params = model_info.params.model_dump() + params = model_info.params.model_dump() + form_data = apply_model_params_to_body(params, form_data) + form_data = apply_model_system_prompt_to_body(params, form_data, user) - if model_info.params: - if model_info.params.get("temperature", None) is not None: - form_data["temperature"] = float(model_info.params.get("temperature")) + pipe_id = get_pipe_id(form_data) + function_module = get_function_module(pipe_id) - if model_info.params.get("top_p", None): - form_data["top_p"] = int(model_info.params.get("top_p", None)) + pipe = function_module.pipe + params = get_function_params( + function_module, + form_data, + user, + { + "__event_emitter__": __event_emitter__, + "__event_call__": __event_call__, + "__task__": __task__, + }, + ) - if model_info.params.get("max_tokens", None): - form_data["max_tokens"] = int(model_info.params.get("max_tokens", None)) - - if model_info.params.get("frequency_penalty", None): - form_data["frequency_penalty"] = int( - model_info.params.get("frequency_penalty", None) - ) - - if model_info.params.get("seed", None): - form_data["seed"] = model_info.params.get("seed", None) - - if model_info.params.get("stop", None): - form_data["stop"] = ( - [ - bytes(stop, "utf-8").decode("unicode_escape") - for stop in model_info.params["stop"] - ] - if model_info.params.get("stop", None) - else None - ) - - system = model_info.params.get("system", None) - if system: - system = prompt_template( - system, - **( - { - "user_name": user.name, - "user_location": ( - user.info.get("location") if user.info else None - ), - } - if user - else {} - ), - ) - # Check if the payload already has a system message - # If not, add a system message to the payload - if form_data.get("messages"): - for message in form_data["messages"]: - if message.get("role") == "system": - message["content"] = system + message["content"] - break - else: - form_data["messages"].insert( - 0, - { - "role": "system", - "content": system, - }, - ) - - else: - pass - - async def job(): - pipe_id = form_data["model"] - if "." in pipe_id: - pipe_id, sub_pipe_id = pipe_id.split(".", 1) - print(pipe_id) - - # Check if function is already loaded - if pipe_id not in app.state.FUNCTIONS: - function_module, function_type, frontmatter = load_function_module_by_id( - pipe_id - ) - app.state.FUNCTIONS[pipe_id] = function_module - else: - function_module = app.state.FUNCTIONS[pipe_id] - - if hasattr(function_module, "valves") and hasattr(function_module, "Valves"): - - valves = Functions.get_function_valves_by_id(pipe_id) - function_module.valves = function_module.Valves( - **(valves if valves else {}) - ) - - pipe = function_module.pipe - - # Get the signature of the function - sig = inspect.signature(pipe) - params = {"body": form_data} - - if "__user__" in sig.parameters: - __user__ = { - "id": user.id, - "email": user.email, - "name": user.name, - "role": user.role, - } + if form_data["stream"]: + async def stream_content(): try: - if hasattr(function_module, "UserValves"): - __user__["valves"] = function_module.UserValves( - **Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id) - ) - except Exception as e: - print(e) + res = await execute_pipe(pipe, params) - params = {**params, "__user__": __user__} - - if "__event_emitter__" in sig.parameters: - params = {**params, "__event_emitter__": __event_emitter__} - - if "__event_call__" in sig.parameters: - params = {**params, "__event_call__": __event_call__} - - if "__task__" in sig.parameters: - params = {**params, "__task__": __task__} - - if form_data["stream"]: - - async def stream_content(): - try: - if inspect.iscoroutinefunction(pipe): - res = await pipe(**params) - else: - res = pipe(**params) - - # Directly return if the response is a StreamingResponse - if isinstance(res, StreamingResponse): - async for data in res.body_iterator: - yield data - return - if isinstance(res, dict): - yield f"data: {json.dumps(res)}\n\n" - return - - except Exception as e: - print(f"Error: {e}") - yield f"data: {json.dumps({'error': {'detail':str(e)}})}\n\n" + # Directly return if the response is a StreamingResponse + if isinstance(res, StreamingResponse): + async for data in res.body_iterator: + yield data + return + if isinstance(res, dict): + yield f"data: {json.dumps(res)}\n\n" return - if isinstance(res, str): - message = stream_message_template(form_data["model"], res) - yield f"data: {json.dumps(message)}\n\n" - - if isinstance(res, Iterator): - for line in res: - if isinstance(line, BaseModel): - line = line.model_dump_json() - line = f"data: {line}" - if isinstance(line, dict): - line = f"data: {json.dumps(line)}" - - try: - line = line.decode("utf-8") - except: - pass - - if line.startswith("data:"): - yield f"{line}\n\n" - else: - line = stream_message_template(form_data["model"], line) - yield f"data: {json.dumps(line)}\n\n" - - if isinstance(res, str) or isinstance(res, Generator): - finish_message = { - "id": f"{form_data['model']}-{str(uuid.uuid4())}", - "object": "chat.completion.chunk", - "created": int(time.time()), - "model": form_data["model"], - "choices": [ - { - "index": 0, - "delta": {}, - "logprobs": None, - "finish_reason": "stop", - } - ], - } - - yield f"data: {json.dumps(finish_message)}\n\n" - yield f"data: [DONE]" - - if isinstance(res, AsyncGenerator): - async for line in res: - if isinstance(line, BaseModel): - line = line.model_dump_json() - line = f"data: {line}" - if isinstance(line, dict): - line = f"data: {json.dumps(line)}" - - try: - line = line.decode("utf-8") - except: - pass - - if line.startswith("data:"): - yield f"{line}\n\n" - else: - line = stream_message_template(form_data["model"], line) - yield f"data: {json.dumps(line)}\n\n" - - return StreamingResponse(stream_content(), media_type="text/event-stream") - else: - - try: - if inspect.iscoroutinefunction(pipe): - res = await pipe(**params) - else: - res = pipe(**params) - - if isinstance(res, StreamingResponse): - return res except Exception as e: print(f"Error: {e}") - return {"error": {"detail": str(e)}} + yield f"data: {json.dumps({'error': {'detail':str(e)}})}\n\n" + return - if isinstance(res, dict): - return res - elif isinstance(res, BaseModel): - return res.model_dump() - else: - message = "" - if isinstance(res, str): - message = res - elif isinstance(res, Generator): - for stream in res: - message = f"{message}{stream}" - elif isinstance(res, AsyncGenerator): - async for stream in res: - message = f"{message}{stream}" + if isinstance(res, str): + message = openai_chat_chunk_message_template(form_data["model"], res) + yield f"data: {json.dumps(message)}\n\n" - return { - "id": f"{form_data['model']}-{str(uuid.uuid4())}", - "object": "chat.completion", - "created": int(time.time()), - "model": form_data["model"], - "choices": [ - { - "index": 0, - "message": { - "role": "assistant", - "content": message, - }, - "logprobs": None, - "finish_reason": "stop", - } - ], - } + if isinstance(res, Iterator): + for line in res: + yield process_line(form_data, line) - return await job() + if isinstance(res, AsyncGenerator): + async for line in res: + yield process_line(form_data, line) + + if isinstance(res, str) or isinstance(res, Generator): + finish_message = openai_chat_chunk_message_template( + form_data["model"], "" + ) + finish_message["choices"][0]["finish_reason"] = "stop" + yield f"data: {json.dumps(finish_message)}\n\n" + yield "data: [DONE]" + + return StreamingResponse(stream_content(), media_type="text/event-stream") + else: + try: + res = await execute_pipe(pipe, params) + + except Exception as e: + print(f"Error: {e}") + return {"error": {"detail": str(e)}} + + if isinstance(res, StreamingResponse) or isinstance(res, dict): + return res + if isinstance(res, BaseModel): + return res.model_dump() + + message = await get_message_content(res) + return openai_chat_completion_message_template(form_data["model"], message) diff --git a/backend/apps/webui/models/models.py b/backend/apps/webui/models/models.py index 3b128c7d6..8277d1d0b 100644 --- a/backend/apps/webui/models/models.py +++ b/backend/apps/webui/models/models.py @@ -1,13 +1,11 @@ -import json import logging -from typing import Optional +from typing import Optional, List from pydantic import BaseModel, ConfigDict -from sqlalchemy import String, Column, BigInteger, Text +from sqlalchemy import Column, BigInteger, Text from apps.webui.internal.db import Base, JSONField, get_db -from typing import List, Union, Optional from config import SRC_LOG_LEVELS import time @@ -113,7 +111,6 @@ class ModelForm(BaseModel): class ModelsTable: - def insert_new_model( self, form_data: ModelForm, user_id: str ) -> Optional[ModelModel]: @@ -126,9 +123,7 @@ class ModelsTable: } ) try: - with get_db() as db: - result = Model(**model.model_dump()) db.add(result) db.commit() @@ -144,13 +139,11 @@ class ModelsTable: def get_all_models(self) -> List[ModelModel]: with get_db() as db: - return [ModelModel.model_validate(model) for model in db.query(Model).all()] def get_model_by_id(self, id: str) -> Optional[ModelModel]: try: with get_db() as db: - model = db.get(Model, id) return ModelModel.model_validate(model) except: @@ -178,7 +171,6 @@ class ModelsTable: def delete_model_by_id(self, id: str) -> bool: try: with get_db() as db: - db.query(Model).filter_by(id=id).delete() db.commit() diff --git a/backend/apps/webui/routers/utils.py b/backend/apps/webui/routers/utils.py index 780ed6b43..4ffe748b0 100644 --- a/backend/apps/webui/routers/utils.py +++ b/backend/apps/webui/routers/utils.py @@ -1,3 +1,6 @@ +from pathlib import Path +import site + from fastapi import APIRouter, UploadFile, File, Response from fastapi import Depends, HTTPException, status from starlette.responses import StreamingResponse, FileResponse @@ -64,8 +67,18 @@ async def download_chat_as_pdf( pdf = FPDF() pdf.add_page() - STATIC_DIR = "./static" - FONTS_DIR = f"{STATIC_DIR}/fonts" + # When running in docker, workdir is /app/backend, so fonts is in /app/backend/static/fonts + FONTS_DIR = Path("./static/fonts") + + # Non Docker Installation + + # When running using `pip install` the static directory is in the site packages. + if not FONTS_DIR.exists(): + FONTS_DIR = Path(site.getsitepackages()[0]) / "static/fonts" + # When running using `pip install -e .` the static directory is in the site packages. + # This path only works if `open-webui serve` is run from the root of this project. + if not FONTS_DIR.exists(): + FONTS_DIR = Path("./backend/static/fonts") pdf.add_font("NotoSans", "", f"{FONTS_DIR}/NotoSans-Regular.ttf") pdf.add_font("NotoSans", "b", f"{FONTS_DIR}/NotoSans-Bold.ttf") diff --git a/backend/config.py b/backend/config.py index 395720c18..e976b226d 100644 --- a/backend/config.py +++ b/backend/config.py @@ -349,6 +349,12 @@ GOOGLE_OAUTH_SCOPE = PersistentConfig( os.environ.get("GOOGLE_OAUTH_SCOPE", "openid email profile"), ) +GOOGLE_REDIRECT_URI = PersistentConfig( + "GOOGLE_REDIRECT_URI", + "oauth.google.redirect_uri", + os.environ.get("GOOGLE_REDIRECT_URI", ""), +) + MICROSOFT_CLIENT_ID = PersistentConfig( "MICROSOFT_CLIENT_ID", "oauth.microsoft.client_id", @@ -373,6 +379,12 @@ MICROSOFT_OAUTH_SCOPE = PersistentConfig( os.environ.get("MICROSOFT_OAUTH_SCOPE", "openid email profile"), ) +MICROSOFT_REDIRECT_URI = PersistentConfig( + "MICROSOFT_REDIRECT_URI", + "oauth.microsoft.redirect_uri", + os.environ.get("MICROSOFT_REDIRECT_URI", ""), +) + OAUTH_CLIENT_ID = PersistentConfig( "OAUTH_CLIENT_ID", "oauth.oidc.client_id", @@ -391,6 +403,12 @@ OPENID_PROVIDER_URL = PersistentConfig( os.environ.get("OPENID_PROVIDER_URL", ""), ) +OPENID_REDIRECT_URI = PersistentConfig( + "OPENID_REDIRECT_URI", + "oauth.oidc.redirect_uri", + os.environ.get("OPENID_REDIRECT_URI", ""), +) + OAUTH_SCOPES = PersistentConfig( "OAUTH_SCOPES", "oauth.oidc.scopes", @@ -424,6 +442,7 @@ def load_oauth_providers(): "client_secret": GOOGLE_CLIENT_SECRET.value, "server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration", "scope": GOOGLE_OAUTH_SCOPE.value, + "redirect_uri": GOOGLE_REDIRECT_URI.value, } if ( @@ -436,6 +455,7 @@ def load_oauth_providers(): "client_secret": MICROSOFT_CLIENT_SECRET.value, "server_metadata_url": f"https://login.microsoftonline.com/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration", "scope": MICROSOFT_OAUTH_SCOPE.value, + "redirect_uri": MICROSOFT_REDIRECT_URI.value, } if ( @@ -449,6 +469,7 @@ def load_oauth_providers(): "server_metadata_url": OPENID_PROVIDER_URL.value, "scope": OAUTH_SCOPES.value, "name": OAUTH_PROVIDER_NAME.value, + "redirect_uri": OPENID_REDIRECT_URI.value, } @@ -1281,6 +1302,24 @@ COMFYUI_SD3 = PersistentConfig( os.environ.get("COMFYUI_SD3", "").lower() == "true", ) +COMFYUI_FLUX = PersistentConfig( + "COMFYUI_FLUX", + "image_generation.comfyui.flux", + os.environ.get("COMFYUI_FLUX", "").lower() == "true", +) + +COMFYUI_FLUX_WEIGHT_DTYPE = PersistentConfig( + "COMFYUI_FLUX_WEIGHT_DTYPE", + "image_generation.comfyui.flux_weight_dtype", + os.getenv("COMFYUI_FLUX_WEIGHT_DTYPE", ""), +) + +COMFYUI_FLUX_FP8_CLIP = PersistentConfig( + "COMFYUI_FLUX_FP8_CLIP", + "image_generation.comfyui.flux_fp8_clip", + os.getenv("COMFYUI_FLUX_FP8_CLIP", ""), +) + IMAGES_OPENAI_API_BASE_URL = PersistentConfig( "IMAGES_OPENAI_API_BASE_URL", "image_generation.openai.api_base_url", diff --git a/backend/main.py b/backend/main.py index 360f5f415..a7dd8bc23 100644 --- a/backend/main.py +++ b/backend/main.py @@ -13,8 +13,6 @@ import aiohttp import requests import mimetypes import shutil -import os -import uuid import inspect from fastapi import FastAPI, Request, Depends, status, UploadFile, File, Form @@ -29,7 +27,7 @@ from starlette.middleware.sessions import SessionMiddleware from starlette.responses import StreamingResponse, Response, RedirectResponse -from apps.socket.main import sio, app as socket_app, get_event_emitter, get_event_call +from apps.socket.main import app as socket_app, get_event_emitter, get_event_call from apps.ollama.main import ( app as ollama_app, get_all_models as get_ollama_models, @@ -619,32 +617,15 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware): content={"detail": str(e)}, ) - # Extract valves from the request body - valves = None - if "valves" in body: - valves = body["valves"] - del body["valves"] + metadata = { + "chat_id": body.pop("chat_id", None), + "message_id": body.pop("id", None), + "session_id": body.pop("session_id", None), + "valves": body.pop("valves", None), + } - # Extract session_id, chat_id and message_id from the request body - session_id = None - if "session_id" in body: - session_id = body["session_id"] - del body["session_id"] - chat_id = None - if "chat_id" in body: - chat_id = body["chat_id"] - del body["chat_id"] - message_id = None - if "id" in body: - message_id = body["id"] - del body["id"] - - __event_emitter__ = await get_event_emitter( - {"chat_id": chat_id, "message_id": message_id, "session_id": session_id} - ) - __event_call__ = await get_event_call( - {"chat_id": chat_id, "message_id": message_id, "session_id": session_id} - ) + __event_emitter__ = get_event_emitter(metadata) + __event_call__ = get_event_call(metadata) # Initialize data_items to store additional data to be sent to the client data_items = [] @@ -709,13 +690,7 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware): if len(citations) > 0: data_items.append({"citations": citations}) - body["metadata"] = { - "session_id": session_id, - "chat_id": chat_id, - "message_id": message_id, - "valves": valves, - } - + body["metadata"] = metadata modified_body_bytes = json.dumps(body).encode("utf-8") # Replace the request body with the modified one request._body = modified_body_bytes @@ -1191,13 +1166,13 @@ async def chat_completed(form_data: dict, user=Depends(get_verified_user)): status_code=r.status_code, content=res, ) - except: + except Exception: pass else: pass - __event_emitter__ = await get_event_emitter( + __event_emitter__ = get_event_emitter( { "chat_id": data["chat_id"], "message_id": data["id"], @@ -1205,7 +1180,7 @@ async def chat_completed(form_data: dict, user=Depends(get_verified_user)): } ) - __event_call__ = await get_event_call( + __event_call__ = get_event_call( { "chat_id": data["chat_id"], "message_id": data["id"], @@ -1310,9 +1285,7 @@ async def chat_completed(form_data: dict, user=Depends(get_verified_user)): @app.post("/api/chat/actions/{action_id}") -async def chat_completed( - action_id: str, form_data: dict, user=Depends(get_verified_user) -): +async def chat_action(action_id: str, form_data: dict, user=Depends(get_verified_user)): if "." in action_id: action_id, sub_action_id = action_id.split(".") else: @@ -1334,14 +1307,14 @@ async def chat_completed( ) model = app.state.MODELS[model_id] - __event_emitter__ = await get_event_emitter( + __event_emitter__ = get_event_emitter( { "chat_id": data["chat_id"], "message_id": data["id"], "session_id": data["session_id"], } ) - __event_call__ = await get_event_call( + __event_call__ = get_event_call( { "chat_id": data["chat_id"], "message_id": data["id"], @@ -1770,7 +1743,6 @@ class AddPipelineForm(BaseModel): @app.post("/api/pipelines/add") async def add_pipeline(form_data: AddPipelineForm, user=Depends(get_admin_user)): - r = None try: urlIdx = form_data.urlIdx @@ -1813,7 +1785,6 @@ class DeletePipelineForm(BaseModel): @app.delete("/api/pipelines/delete") async def delete_pipeline(form_data: DeletePipelineForm, user=Depends(get_admin_user)): - r = None try: urlIdx = form_data.urlIdx @@ -1891,7 +1862,6 @@ async def get_pipeline_valves( models = await get_all_models() r = None try: - url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx] key = openai_app.state.config.OPENAI_API_KEYS[urlIdx] @@ -2143,6 +2113,7 @@ for provider_name, provider_config in OAUTH_PROVIDERS.items(): client_kwargs={ "scope": provider_config["scope"], }, + redirect_uri=provider_config["redirect_uri"], ) # SessionMiddleware is used by authlib for oauth @@ -2160,7 +2131,10 @@ if len(OAUTH_PROVIDERS) > 0: async def oauth_login(provider: str, request: Request): if provider not in OAUTH_PROVIDERS: raise HTTPException(404) - redirect_uri = request.url_for("oauth_callback", provider=provider) + # If the provider has a custom redirect URL, use that, otherwise automatically generate one + redirect_uri = OAUTH_PROVIDERS[provider].get("redirect_uri") or request.url_for( + "oauth_callback", provider=provider + ) return await oauth.create_client(provider).authorize_redirect(request, redirect_uri) diff --git a/backend/requirements.txt b/backend/requirements.txt index 8b12854a0..c22712abf 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -12,6 +12,7 @@ passlib[bcrypt]==1.7.4 requests==2.32.3 aiohttp==3.9.5 + sqlalchemy==2.0.31 alembic==1.13.2 peewee==3.17.6 @@ -19,7 +20,7 @@ peewee-migrate==1.12.2 psycopg2-binary==2.9.9 PyMySQL==1.1.1 bcrypt==4.1.3 -SQLAlchemy + pymongo redis boto3==1.34.110 diff --git a/backend/utils/misc.py b/backend/utils/misc.py index f44a7ce7a..c4e2eda6f 100644 --- a/backend/utils/misc.py +++ b/backend/utils/misc.py @@ -1,6 +1,5 @@ from pathlib import Path import hashlib -import json import re from datetime import timedelta from typing import Optional, List, Tuple @@ -8,37 +7,39 @@ import uuid import time -def get_last_user_message_item(messages: List[dict]) -> str: +def get_last_user_message_item(messages: List[dict]) -> Optional[dict]: for message in reversed(messages): if message["role"] == "user": return message return None -def get_last_user_message(messages: List[dict]) -> str: - message = get_last_user_message_item(messages) - - if message is not None: - if isinstance(message["content"], list): - for item in message["content"]: - if item["type"] == "text": - return item["text"] +def get_content_from_message(message: dict) -> Optional[str]: + if isinstance(message["content"], list): + for item in message["content"]: + if item["type"] == "text": + return item["text"] + else: return message["content"] return None -def get_last_assistant_message(messages: List[dict]) -> str: +def get_last_user_message(messages: List[dict]) -> Optional[str]: + message = get_last_user_message_item(messages) + if message is None: + return None + + return get_content_from_message(message) + + +def get_last_assistant_message(messages: List[dict]) -> Optional[str]: for message in reversed(messages): if message["role"] == "assistant": - if isinstance(message["content"], list): - for item in message["content"]: - if item["type"] == "text": - return item["text"] - return message["content"] + return get_content_from_message(message) return None -def get_system_message(messages: List[dict]) -> dict: +def get_system_message(messages: List[dict]) -> Optional[dict]: for message in messages: if message["role"] == "system": return message @@ -49,7 +50,7 @@ def remove_system_message(messages: List[dict]) -> List[dict]: return [message for message in messages if message["role"] != "system"] -def pop_system_message(messages: List[dict]) -> Tuple[dict, List[dict]]: +def pop_system_message(messages: List[dict]) -> Tuple[Optional[dict], List[dict]]: return get_system_message(messages), remove_system_message(messages) @@ -87,23 +88,29 @@ def add_or_update_system_message(content: str, messages: List[dict]): return messages -def stream_message_template(model: str, message: str): +def openai_chat_message_template(model: str): return { "id": f"{model}-{str(uuid.uuid4())}", - "object": "chat.completion.chunk", "created": int(time.time()), "model": model, - "choices": [ - { - "index": 0, - "delta": {"content": message}, - "logprobs": None, - "finish_reason": None, - } - ], + "choices": [{"index": 0, "logprobs": None, "finish_reason": None}], } +def openai_chat_chunk_message_template(model: str, message: str): + template = openai_chat_message_template(model) + template["object"] = "chat.completion.chunk" + template["choices"][0]["delta"] = {"content": message} + return template + + +def openai_chat_completion_message_template(model: str, message: str): + template = openai_chat_message_template(model) + template["object"] = "chat.completion" + template["choices"][0]["message"] = {"content": message, "role": "assistant"} + template["choices"][0]["finish_reason"] = "stop" + + def get_gravatar_url(email): # Trim leading and trailing whitespace from # an email address and force all characters @@ -174,7 +181,7 @@ def extract_folders_after_data_docs(path): tags = [] folders = parts[index_docs:-1] - for idx, part in enumerate(folders): + for idx, _ in enumerate(folders): tags.append("/".join(folders[: idx + 1])) return tags @@ -270,11 +277,11 @@ def parse_ollama_modelfile(model_text): value = param_match.group(1) try: - if param_type == int: + if param_type is int: value = int(value) - elif param_type == float: + elif param_type is float: value = float(value) - elif param_type == bool: + elif param_type is bool: value = value.lower() == "true" except Exception as e: print(e) diff --git a/package-lock.json b/package-lock.json index cf04da5c6..f3e8d2e38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.3.10", + "version": "0.3.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.3.10", + "version": "0.3.11", "dependencies": { "@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-python": "^6.1.6", @@ -20,6 +20,7 @@ "dayjs": "^1.11.10", "eventsource-parser": "^1.1.2", "file-saver": "^2.0.5", + "fuse.js": "^7.0.0", "highlight.js": "^11.9.0", "i18next": "^23.10.0", "i18next-browser-languagedetector": "^7.2.0", @@ -4820,6 +4821,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fuse.js": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.0.0.tgz", + "integrity": "sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==", + "engines": { + "node": ">=10" + } + }, "node_modules/gc-hook": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.3.1.tgz", diff --git a/package.json b/package.json index f7cc12598..6687cef75 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.3.10", + "version": "0.3.11", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", @@ -60,6 +60,7 @@ "dayjs": "^1.11.10", "eventsource-parser": "^1.1.2", "file-saver": "^2.0.5", + "fuse.js": "^7.0.0", "highlight.js": "^11.9.0", "i18next": "^23.10.0", "i18next-browser-languagedetector": "^7.2.0", diff --git a/pyproject.toml b/pyproject.toml index efce1158f..eea77cfd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,50 +8,61 @@ license = { file = "LICENSE" } dependencies = [ "fastapi==0.111.0", "uvicorn[standard]==0.22.0", - "pydantic==2.7.1", + "pydantic==2.8.2", "python-multipart==0.0.9", "Flask==3.0.3", "Flask-Cors==4.0.1", - "python-socketio==5.11.2", + "python-socketio==5.11.3", "python-jose==3.3.0", "passlib[bcrypt]==1.7.4", - "requests==2.32.2", + "requests==2.32.3", "aiohttp==3.9.5", - "peewee==3.17.5", + + "sqlalchemy==2.0.31", + "alembic==1.13.2", + "peewee==3.17.6", "peewee-migrate==1.12.2", "psycopg2-binary==2.9.9", "PyMySQL==1.1.1", "bcrypt==4.1.3", + "pymongo", + "redis", "boto3==1.34.110", "argon2-cffi==23.1.0", "APScheduler==3.10.4", - "google-generativeai==0.5.4", - "langchain==0.2.0", - "langchain-community==0.2.9", - "langchain-chroma==0.1.1", + "openai", + "anthropic", + "google-generativeai==0.5.4", + "tiktoken", + + "langchain==0.2.11", + "langchain-community==0.2.10", + "langchain-chroma==0.1.2", "fake-useragent==1.5.1", - "chromadb==0.5.0", - "sentence-transformers==2.7.0", + "chromadb==0.5.4", + "sentence-transformers==3.0.1", "pypdf==4.2.0", "docx2txt==0.8", - "unstructured==0.14.0", + "python-pptx==0.6.23", + "unstructured==0.15.0", "Markdown==3.6", "pypandoc==1.13", "pandas==2.2.2", - "openpyxl==3.1.2", + "openpyxl==3.1.5", "pyxlsb==1.0.10", "xlrd==2.0.1", "validators==0.28.1", + "psutil", - "opencv-python-headless==4.9.0.80", - "rapidocr-onnxruntime==1.3.22", + "opencv-python-headless==4.10.0.84", + "rapidocr-onnxruntime==1.3.24", "fpdf2==2.7.9", "rank-bm25==0.2.2", @@ -62,13 +73,17 @@ dependencies = [ "authlib==1.3.1", "black==24.4.2", - "langfuse==2.33.0", + "langfuse==2.39.2", "youtube-transcript-api==0.6.2", "pytube==15.0.0", + "extract_msg", "pydub", - "duckduckgo-search~=6.1.5" + "duckduckgo-search~=6.2.1", + "docker~=7.1.0", + "pytest~=8.2.2", + "pytest-docker~=3.1.1" ] readme = "README.md" requires-python = ">= 3.11, < 3.12.0a1" diff --git a/requirements-dev.lock b/requirements-dev.lock index e56ad08f0..5380b66b2 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -16,10 +16,17 @@ aiohttp==3.9.5 # via open-webui aiosignal==1.3.1 # via aiohttp +alembic==1.13.2 + # via open-webui annotated-types==0.6.0 # via pydantic -anyio==4.3.0 +anthropic==0.32.0 + # via open-webui +anyio==4.4.0 + # via anthropic # via httpx + # via langfuse + # via openai # via starlette # via watchfiles apscheduler==3.10.4 @@ -32,6 +39,7 @@ asgiref==3.8.1 # via opentelemetry-instrumentation-asgi attrs==23.2.0 # via aiohttp + # via pytest-docker authlib==1.3.1 # via open-webui av==11.0.0 @@ -76,9 +84,9 @@ chardet==5.2.0 charset-normalizer==3.3.2 # via requests # via unstructured-client -chroma-hnswlib==0.7.3 +chroma-hnswlib==0.7.5 # via chromadb -chromadb==0.5.0 +chromadb==0.5.4 # via langchain-chroma # via open-webui click==8.1.7 @@ -102,7 +110,6 @@ cryptography==42.0.7 ctranslate2==4.2.1 # via faster-whisper dataclasses-json==0.6.6 - # via langchain # via langchain-community # via unstructured # via unstructured-client @@ -113,11 +120,17 @@ defusedxml==0.7.1 deprecated==1.2.14 # via opentelemetry-api # via opentelemetry-exporter-otlp-proto-grpc +distro==1.9.0 + # via anthropic + # via openai dnspython==2.6.1 # via email-validator + # via pymongo +docker==7.1.0 + # via open-webui docx2txt==0.8 # via open-webui -duckduckgo-search==6.1.5 +duckduckgo-search==6.2.6 # via open-webui easygui==0.98.3 # via oletools @@ -208,8 +221,11 @@ httplib2==0.22.0 httptools==0.6.1 # via uvicorn httpx==0.27.0 + # via anthropic + # via chromadb # via fastapi # via langfuse + # via openai huggingface-hub==0.23.0 # via faster-whisper # via sentence-transformers @@ -229,12 +245,16 @@ importlib-metadata==7.0.0 # via opentelemetry-api importlib-resources==6.4.0 # via chromadb +iniconfig==2.0.0 + # via pytest itsdangerous==2.2.0 # via flask jinja2==3.1.4 # via fastapi # via flask # via torch +jiter==0.5.0 + # via anthropic jmespath==1.0.1 # via boto3 # via botocore @@ -249,14 +269,14 @@ jsonpointer==2.4 # via jsonpatch kubernetes==29.0.0 # via chromadb -langchain==0.2.0 +langchain==0.2.11 # via langchain-community # via open-webui -langchain-chroma==0.1.1 +langchain-chroma==0.1.2 # via open-webui -langchain-community==0.2.0 +langchain-community==0.2.10 # via open-webui -langchain-core==0.2.1 +langchain-core==0.2.28 # via langchain # via langchain-chroma # via langchain-community @@ -265,22 +285,26 @@ langchain-text-splitters==0.2.0 # via langchain langdetect==1.0.9 # via unstructured -langfuse==2.33.0 +langfuse==2.39.2 # via open-webui -langsmith==0.1.57 +langsmith==0.1.96 # via langchain # via langchain-community # via langchain-core lark==1.1.8 # via rtfde lxml==5.2.2 + # via python-pptx # via unstructured +mako==1.3.5 + # via alembic markdown==3.6 # via open-webui markdown-it-py==3.0.0 # via rich markupsafe==2.1.5 # via jinja2 + # via mako # via werkzeug marshmallow==3.21.2 # via dataclasses-json @@ -339,11 +363,13 @@ onnxruntime==1.17.3 # via chromadb # via faster-whisper # via rapidocr-onnxruntime +openai==1.38.0 + # via open-webui opencv-python==4.9.0.80 # via rapidocr-onnxruntime -opencv-python-headless==4.9.0.80 +opencv-python-headless==4.10.0.84 # via open-webui -openpyxl==3.1.2 +openpyxl==3.1.5 # via open-webui opentelemetry-api==1.24.0 # via chromadb @@ -380,7 +406,6 @@ ordered-set==4.1.0 # via deepdiff orjson==3.10.3 # via chromadb - # via duckduckgo-search # via fastapi # via langsmith overrides==7.7.0 @@ -393,6 +418,7 @@ packaging==23.2 # via langfuse # via marshmallow # via onnxruntime + # via pytest # via transformers # via unstructured-client pandas==2.2.2 @@ -403,19 +429,24 @@ pathspec==0.12.1 # via black pcodedmp==1.2.6 # via oletools -peewee==3.17.5 +peewee==3.17.6 # via open-webui # via peewee-migrate peewee-migrate==1.12.2 # via open-webui pillow==10.3.0 # via fpdf2 + # via python-pptx # via rapidocr-onnxruntime # via sentence-transformers platformdirs==4.2.1 # via black +pluggy==1.5.0 + # via pytest posthog==3.5.0 # via chromadb +primp==0.5.5 + # via duckduckgo-search proto-plus==1.23.0 # via google-ai-generativelanguage # via google-api-core @@ -428,6 +459,9 @@ protobuf==4.25.3 # via onnxruntime # via opentelemetry-proto # via proto-plus +psutil==6.0.0 + # via open-webui + # via unstructured psycopg2-binary==2.9.9 # via open-webui pyasn1==0.6.0 @@ -440,7 +474,8 @@ pyclipper==1.3.0.post5 # via rapidocr-onnxruntime pycparser==2.22 # via cffi -pydantic==2.7.1 +pydantic==2.8.2 + # via anthropic # via chromadb # via fastapi # via google-generativeai @@ -449,7 +484,8 @@ pydantic==2.7.1 # via langfuse # via langsmith # via open-webui -pydantic-core==2.18.2 + # via openai +pydantic-core==2.20.1 # via pydantic pydub==0.25.1 # via open-webui @@ -457,7 +493,9 @@ pygments==2.18.0 # via rich pyjwt==2.8.0 # via open-webui -pymysql==1.1.0 +pymongo==4.8.0 + # via open-webui +pymysql==1.1.1 # via open-webui pypandoc==1.13 # via open-webui @@ -471,8 +509,11 @@ pypika==0.48.9 # via chromadb pyproject-hooks==1.1.0 # via build -pyreqwest-impersonate==0.4.7 - # via duckduckgo-search +pytest==8.2.2 + # via open-webui + # via pytest-docker +pytest-docker==3.1.1 + # via open-webui python-dateutil==2.9.0.post0 # via botocore # via kubernetes @@ -492,7 +533,9 @@ python-magic==0.4.27 python-multipart==0.0.9 # via fastapi # via open-webui -python-socketio==5.11.2 +python-pptx==0.6.23 + # via open-webui +python-socketio==5.11.3 # via open-webui pytube==15.0.0 # via open-webui @@ -516,15 +559,18 @@ rank-bm25==0.2.2 # via open-webui rapidfuzz==3.9.0 # via unstructured -rapidocr-onnxruntime==1.3.22 +rapidocr-onnxruntime==1.3.24 # via open-webui red-black-tree-mod==1.20 # via extract-msg +redis==5.0.8 + # via open-webui regex==2024.5.10 # via nltk + # via tiktoken # via transformers -requests==2.32.2 - # via chromadb +requests==2.32.3 + # via docker # via google-api-core # via huggingface-hub # via kubernetes @@ -534,6 +580,7 @@ requests==2.32.2 # via open-webui # via posthog # via requests-oauthlib + # via tiktoken # via transformers # via unstructured # via unstructured-client @@ -556,12 +603,12 @@ scikit-learn==1.4.2 scipy==1.13.0 # via scikit-learn # via sentence-transformers -sentence-transformers==2.7.0 +sentence-transformers==3.0.1 # via open-webui setuptools==69.5.1 # via ctranslate2 # via opentelemetry-instrumentation -shapely==2.0.4 +shapely==2.0.5 # via rapidocr-onnxruntime shellingham==1.5.4 # via typer @@ -577,13 +624,17 @@ six==1.16.0 # via rapidocr-onnxruntime # via unstructured-client sniffio==1.3.1 + # via anthropic # via anyio # via httpx + # via openai soupsieve==2.5 # via beautifulsoup4 -sqlalchemy==2.0.30 +sqlalchemy==2.0.31 + # via alembic # via langchain # via langchain-community + # via open-webui starlette==0.37.2 # via fastapi sympy==1.12 @@ -598,7 +649,10 @@ tenacity==8.3.0 # via langchain-core threadpoolctl==3.5.0 # via scikit-learn +tiktoken==0.7.0 + # via open-webui tokenizers==0.15.2 + # via anthropic # via chromadb # via faster-whisper # via transformers @@ -609,18 +663,24 @@ tqdm==4.66.4 # via google-generativeai # via huggingface-hub # via nltk + # via openai # via sentence-transformers # via transformers + # via unstructured transformers==4.39.3 # via sentence-transformers typer==0.12.3 # via chromadb # via fastapi-cli typing-extensions==4.11.0 + # via alembic + # via anthropic # via chromadb # via fastapi # via google-generativeai # via huggingface-hub + # via langchain-core + # via openai # via opentelemetry-sdk # via pydantic # via pydantic-core @@ -640,7 +700,7 @@ tzlocal==5.2 # via extract-msg ujson==5.10.0 # via fastapi -unstructured==0.14.0 +unstructured==0.15.0 # via open-webui unstructured-client==0.22.0 # via unstructured @@ -648,6 +708,7 @@ uritemplate==4.1.1 # via google-api-python-client urllib3==2.2.1 # via botocore + # via docker # via kubernetes # via requests # via unstructured-client @@ -676,6 +737,8 @@ wsproto==1.2.0 # via simple-websocket xlrd==2.0.1 # via open-webui +xlsxwriter==3.2.0 + # via python-pptx yarl==1.9.4 # via aiohttp youtube-transcript-api==0.6.2 diff --git a/requirements.lock b/requirements.lock index e56ad08f0..5380b66b2 100644 --- a/requirements.lock +++ b/requirements.lock @@ -16,10 +16,17 @@ aiohttp==3.9.5 # via open-webui aiosignal==1.3.1 # via aiohttp +alembic==1.13.2 + # via open-webui annotated-types==0.6.0 # via pydantic -anyio==4.3.0 +anthropic==0.32.0 + # via open-webui +anyio==4.4.0 + # via anthropic # via httpx + # via langfuse + # via openai # via starlette # via watchfiles apscheduler==3.10.4 @@ -32,6 +39,7 @@ asgiref==3.8.1 # via opentelemetry-instrumentation-asgi attrs==23.2.0 # via aiohttp + # via pytest-docker authlib==1.3.1 # via open-webui av==11.0.0 @@ -76,9 +84,9 @@ chardet==5.2.0 charset-normalizer==3.3.2 # via requests # via unstructured-client -chroma-hnswlib==0.7.3 +chroma-hnswlib==0.7.5 # via chromadb -chromadb==0.5.0 +chromadb==0.5.4 # via langchain-chroma # via open-webui click==8.1.7 @@ -102,7 +110,6 @@ cryptography==42.0.7 ctranslate2==4.2.1 # via faster-whisper dataclasses-json==0.6.6 - # via langchain # via langchain-community # via unstructured # via unstructured-client @@ -113,11 +120,17 @@ defusedxml==0.7.1 deprecated==1.2.14 # via opentelemetry-api # via opentelemetry-exporter-otlp-proto-grpc +distro==1.9.0 + # via anthropic + # via openai dnspython==2.6.1 # via email-validator + # via pymongo +docker==7.1.0 + # via open-webui docx2txt==0.8 # via open-webui -duckduckgo-search==6.1.5 +duckduckgo-search==6.2.6 # via open-webui easygui==0.98.3 # via oletools @@ -208,8 +221,11 @@ httplib2==0.22.0 httptools==0.6.1 # via uvicorn httpx==0.27.0 + # via anthropic + # via chromadb # via fastapi # via langfuse + # via openai huggingface-hub==0.23.0 # via faster-whisper # via sentence-transformers @@ -229,12 +245,16 @@ importlib-metadata==7.0.0 # via opentelemetry-api importlib-resources==6.4.0 # via chromadb +iniconfig==2.0.0 + # via pytest itsdangerous==2.2.0 # via flask jinja2==3.1.4 # via fastapi # via flask # via torch +jiter==0.5.0 + # via anthropic jmespath==1.0.1 # via boto3 # via botocore @@ -249,14 +269,14 @@ jsonpointer==2.4 # via jsonpatch kubernetes==29.0.0 # via chromadb -langchain==0.2.0 +langchain==0.2.11 # via langchain-community # via open-webui -langchain-chroma==0.1.1 +langchain-chroma==0.1.2 # via open-webui -langchain-community==0.2.0 +langchain-community==0.2.10 # via open-webui -langchain-core==0.2.1 +langchain-core==0.2.28 # via langchain # via langchain-chroma # via langchain-community @@ -265,22 +285,26 @@ langchain-text-splitters==0.2.0 # via langchain langdetect==1.0.9 # via unstructured -langfuse==2.33.0 +langfuse==2.39.2 # via open-webui -langsmith==0.1.57 +langsmith==0.1.96 # via langchain # via langchain-community # via langchain-core lark==1.1.8 # via rtfde lxml==5.2.2 + # via python-pptx # via unstructured +mako==1.3.5 + # via alembic markdown==3.6 # via open-webui markdown-it-py==3.0.0 # via rich markupsafe==2.1.5 # via jinja2 + # via mako # via werkzeug marshmallow==3.21.2 # via dataclasses-json @@ -339,11 +363,13 @@ onnxruntime==1.17.3 # via chromadb # via faster-whisper # via rapidocr-onnxruntime +openai==1.38.0 + # via open-webui opencv-python==4.9.0.80 # via rapidocr-onnxruntime -opencv-python-headless==4.9.0.80 +opencv-python-headless==4.10.0.84 # via open-webui -openpyxl==3.1.2 +openpyxl==3.1.5 # via open-webui opentelemetry-api==1.24.0 # via chromadb @@ -380,7 +406,6 @@ ordered-set==4.1.0 # via deepdiff orjson==3.10.3 # via chromadb - # via duckduckgo-search # via fastapi # via langsmith overrides==7.7.0 @@ -393,6 +418,7 @@ packaging==23.2 # via langfuse # via marshmallow # via onnxruntime + # via pytest # via transformers # via unstructured-client pandas==2.2.2 @@ -403,19 +429,24 @@ pathspec==0.12.1 # via black pcodedmp==1.2.6 # via oletools -peewee==3.17.5 +peewee==3.17.6 # via open-webui # via peewee-migrate peewee-migrate==1.12.2 # via open-webui pillow==10.3.0 # via fpdf2 + # via python-pptx # via rapidocr-onnxruntime # via sentence-transformers platformdirs==4.2.1 # via black +pluggy==1.5.0 + # via pytest posthog==3.5.0 # via chromadb +primp==0.5.5 + # via duckduckgo-search proto-plus==1.23.0 # via google-ai-generativelanguage # via google-api-core @@ -428,6 +459,9 @@ protobuf==4.25.3 # via onnxruntime # via opentelemetry-proto # via proto-plus +psutil==6.0.0 + # via open-webui + # via unstructured psycopg2-binary==2.9.9 # via open-webui pyasn1==0.6.0 @@ -440,7 +474,8 @@ pyclipper==1.3.0.post5 # via rapidocr-onnxruntime pycparser==2.22 # via cffi -pydantic==2.7.1 +pydantic==2.8.2 + # via anthropic # via chromadb # via fastapi # via google-generativeai @@ -449,7 +484,8 @@ pydantic==2.7.1 # via langfuse # via langsmith # via open-webui -pydantic-core==2.18.2 + # via openai +pydantic-core==2.20.1 # via pydantic pydub==0.25.1 # via open-webui @@ -457,7 +493,9 @@ pygments==2.18.0 # via rich pyjwt==2.8.0 # via open-webui -pymysql==1.1.0 +pymongo==4.8.0 + # via open-webui +pymysql==1.1.1 # via open-webui pypandoc==1.13 # via open-webui @@ -471,8 +509,11 @@ pypika==0.48.9 # via chromadb pyproject-hooks==1.1.0 # via build -pyreqwest-impersonate==0.4.7 - # via duckduckgo-search +pytest==8.2.2 + # via open-webui + # via pytest-docker +pytest-docker==3.1.1 + # via open-webui python-dateutil==2.9.0.post0 # via botocore # via kubernetes @@ -492,7 +533,9 @@ python-magic==0.4.27 python-multipart==0.0.9 # via fastapi # via open-webui -python-socketio==5.11.2 +python-pptx==0.6.23 + # via open-webui +python-socketio==5.11.3 # via open-webui pytube==15.0.0 # via open-webui @@ -516,15 +559,18 @@ rank-bm25==0.2.2 # via open-webui rapidfuzz==3.9.0 # via unstructured -rapidocr-onnxruntime==1.3.22 +rapidocr-onnxruntime==1.3.24 # via open-webui red-black-tree-mod==1.20 # via extract-msg +redis==5.0.8 + # via open-webui regex==2024.5.10 # via nltk + # via tiktoken # via transformers -requests==2.32.2 - # via chromadb +requests==2.32.3 + # via docker # via google-api-core # via huggingface-hub # via kubernetes @@ -534,6 +580,7 @@ requests==2.32.2 # via open-webui # via posthog # via requests-oauthlib + # via tiktoken # via transformers # via unstructured # via unstructured-client @@ -556,12 +603,12 @@ scikit-learn==1.4.2 scipy==1.13.0 # via scikit-learn # via sentence-transformers -sentence-transformers==2.7.0 +sentence-transformers==3.0.1 # via open-webui setuptools==69.5.1 # via ctranslate2 # via opentelemetry-instrumentation -shapely==2.0.4 +shapely==2.0.5 # via rapidocr-onnxruntime shellingham==1.5.4 # via typer @@ -577,13 +624,17 @@ six==1.16.0 # via rapidocr-onnxruntime # via unstructured-client sniffio==1.3.1 + # via anthropic # via anyio # via httpx + # via openai soupsieve==2.5 # via beautifulsoup4 -sqlalchemy==2.0.30 +sqlalchemy==2.0.31 + # via alembic # via langchain # via langchain-community + # via open-webui starlette==0.37.2 # via fastapi sympy==1.12 @@ -598,7 +649,10 @@ tenacity==8.3.0 # via langchain-core threadpoolctl==3.5.0 # via scikit-learn +tiktoken==0.7.0 + # via open-webui tokenizers==0.15.2 + # via anthropic # via chromadb # via faster-whisper # via transformers @@ -609,18 +663,24 @@ tqdm==4.66.4 # via google-generativeai # via huggingface-hub # via nltk + # via openai # via sentence-transformers # via transformers + # via unstructured transformers==4.39.3 # via sentence-transformers typer==0.12.3 # via chromadb # via fastapi-cli typing-extensions==4.11.0 + # via alembic + # via anthropic # via chromadb # via fastapi # via google-generativeai # via huggingface-hub + # via langchain-core + # via openai # via opentelemetry-sdk # via pydantic # via pydantic-core @@ -640,7 +700,7 @@ tzlocal==5.2 # via extract-msg ujson==5.10.0 # via fastapi -unstructured==0.14.0 +unstructured==0.15.0 # via open-webui unstructured-client==0.22.0 # via unstructured @@ -648,6 +708,7 @@ uritemplate==4.1.1 # via google-api-python-client urllib3==2.2.1 # via botocore + # via docker # via kubernetes # via requests # via unstructured-client @@ -676,6 +737,8 @@ wsproto==1.2.0 # via simple-websocket xlrd==2.0.1 # via open-webui +xlsxwriter==3.2.0 + # via python-pptx yarl==1.9.4 # via aiohttp youtube-transcript-api==0.6.2 diff --git a/src/app.css b/src/app.css index c3388f1d3..69e107be9 100644 --- a/src/app.css +++ b/src/app.css @@ -154,3 +154,7 @@ input[type='number'] { .tippy-box[data-theme~='dark'] { @apply rounded-lg bg-gray-950 text-xs border border-gray-900 shadow-xl; } + +.password { + -webkit-text-security: disc; +} diff --git a/src/lib/apis/audio/index.ts b/src/lib/apis/audio/index.ts index 9716c552a..af09af990 100644 --- a/src/lib/apis/audio/index.ts +++ b/src/lib/apis/audio/index.ts @@ -131,3 +131,59 @@ export const synthesizeOpenAISpeech = async ( return res; }; + +export const getModels = async (token: string = '') => { + let error = null; + + const res = await fetch(`${AUDIO_API_BASE_URL}/models`, { + 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) => { + error = err.detail; + console.log(err); + + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getVoices = async (token: string = '') => { + let error = null; + + const res = await fetch(`${AUDIO_API_BASE_URL}/voices`, { + 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) => { + error = err.detail; + console.log(err); + + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/components/admin/Settings/Audio.svelte b/src/lib/components/admin/Settings/Audio.svelte index 50ce7418e..7c3300568 100644 --- a/src/lib/components/admin/Settings/Audio.svelte +++ b/src/lib/components/admin/Settings/Audio.svelte @@ -1,13 +1,19 @@ @@ -185,13 +198,15 @@ class=" dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" bind:value={TTS_ENGINE} placeholder="Select a mode" - on:change={(e) => { + on:change={async (e) => { + await updateConfigHandler(); + await getVoices(); + await getModels(); + if (e.target.value === 'openai') { - getOpenAIVoices(); TTS_VOICE = 'alloy'; TTS_MODEL = 'tts-1'; } else { - getWebAPIVoices(); TTS_VOICE = ''; TTS_MODEL = ''; } @@ -268,7 +283,7 @@ {#each voices as voice} - {/each} @@ -279,15 +294,15 @@
- + {#each models as model} -
@@ -309,7 +324,7 @@ {#each voices as voice} - {/each}
@@ -320,15 +335,15 @@
- + {#each models as model} -
diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 5a3f6f5ae..a2a0b4de8 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -111,7 +111,6 @@ }; let params = {}; - let valves = {}; $: if (history.currentId !== null) { let _messages = []; @@ -285,6 +284,10 @@ if ($page.url.searchParams.get('q')) { prompt = $page.url.searchParams.get('q') ?? ''; + selectedToolIds = ($page.url.searchParams.get('tool_ids') ?? '') + .split(',') + .map((id) => id.trim()) + .filter((id) => id); if (prompt) { await tick(); @@ -821,7 +824,6 @@ keep_alive: $settings.keepAlive ?? undefined, tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined, files: files.length > 0 ? files : undefined, - ...(Object.keys(valves).length ? { valves } : {}), session_id: $socket?.id, chat_id: $chatId, id: responseMessageId @@ -1123,7 +1125,6 @@ max_tokens: params?.max_tokens ?? $settings?.params?.max_tokens ?? undefined, tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined, files: files.length > 0 ? files : undefined, - ...(Object.keys(valves).length ? { valves } : {}), session_id: $socket?.id, chat_id: $chatId, id: responseMessageId @@ -1654,7 +1655,6 @@ bind:show={showControls} bind:chatFiles bind:params - bind:valves />
{/if} diff --git a/src/lib/components/chat/ChatControls.svelte b/src/lib/components/chat/ChatControls.svelte index f67e6d6ef..3de095b0d 100644 --- a/src/lib/components/chat/ChatControls.svelte +++ b/src/lib/components/chat/ChatControls.svelte @@ -9,9 +9,7 @@ export let models = []; export let chatId = null; - export let chatFiles = []; - export let valves = {}; export let params = {}; let largeScreen = false; @@ -50,7 +48,6 @@ }} {models} bind:chatFiles - bind:valves bind:params /> @@ -66,7 +63,6 @@ }} {models} bind:chatFiles - bind:valves bind:params /> diff --git a/src/lib/components/chat/Controls/Controls.svelte b/src/lib/components/chat/Controls/Controls.svelte index 4e4198e53..69034a305 100644 --- a/src/lib/components/chat/Controls/Controls.svelte +++ b/src/lib/components/chat/Controls/Controls.svelte @@ -5,14 +5,13 @@ import XMark from '$lib/components/icons/XMark.svelte'; import AdvancedParams from '../Settings/Advanced/AdvancedParams.svelte'; - import Valves from '$lib/components/common/Valves.svelte'; + import Valves from '$lib/components/chat/Controls/Valves.svelte'; import FileItem from '$lib/components/common/FileItem.svelte'; import Collapsible from '$lib/components/common/Collapsible.svelte'; export let models = []; export let chatFiles = []; - export let valves = {}; export let params = {}; @@ -39,6 +38,7 @@ url={`${file?.url}`} name={file.name} type={file.type} + size={file?.size} dismissible={true} on:dismiss={() => { // Remove the file from the chatFiles array @@ -54,17 +54,13 @@
{/if} - {#if models.length === 1 && models[0]?.pipe?.valves_spec} -
-
{$i18n.t('Valves')}
- -
- -
+ +
+
+
-
- {/if} +
diff --git a/src/lib/components/chat/Settings/Valves.svelte b/src/lib/components/chat/Controls/Valves.svelte similarity index 75% rename from src/lib/components/chat/Settings/Valves.svelte rename to src/lib/components/chat/Controls/Valves.svelte index 579779162..8cf7ac39b 100644 --- a/src/lib/components/chat/Settings/Valves.svelte +++ b/src/lib/components/chat/Controls/Valves.svelte @@ -15,18 +15,14 @@ updateUserValvesById as updateFunctionUserValvesById } from '$lib/apis/functions'; - import ManageModal from './Personalization/ManageModal.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte'; import Spinner from '$lib/components/common/Spinner.svelte'; - import Switch from '$lib/components/common/Switch.svelte'; import Valves from '$lib/components/common/Valves.svelte'; const dispatch = createEventDispatcher(); const i18n = getContext('i18n'); - export let saveSettings: Function; - let tab = 'tools'; let selectedId = ''; @@ -35,6 +31,19 @@ let valvesSpec = null; let valves = {}; + let debounceTimer; + + const debounceSubmitHandler = async () => { + if (debounceTimer) { + clearTimeout(debounceTimer); + } + + // Set a new timer + debounceTimer = setTimeout(() => { + submitHandler(); + }, 500); // 0.5 second debounce + }; + const getUserValves = async () => { loading = true; if (tab === 'tools') { @@ -112,53 +121,45 @@ dispatch('save'); }} > -
-
-
- -
- {$i18n.t('Manage Valves')} -
-
- -
- -
-
-
- +
+
+ +
+ @@ -167,24 +168,21 @@
{#if selectedId} -
+
-
+
{#if !loading} - + { + debounceSubmitHandler(); + }} + /> {:else} {/if}
{/if}
- -
- -
diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 5ea541c7f..2e1e4b98e 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -98,6 +98,7 @@ const uploadFileHandler = async (file) => { console.log(file); + // Check if the file is an audio file and transcribe/convert it to text file if (['audio/mpeg', 'audio/wav'].includes(file['type'])) { const res = await transcribeAudio(localStorage.token, file).catch((error) => { @@ -112,40 +113,49 @@ } } - // Upload the file to the server - const uploadedFile = await uploadFile(localStorage.token, file).catch((error) => { - toast.error(error); - return null; - }); + const fileItem = { + type: 'file', + file: '', + id: null, + url: '', + name: file.name, + collection_name: '', + status: '', + size: file.size, + error: '' + }; + files = [...files, fileItem]; - if (uploadedFile) { - const fileItem = { - type: 'file', - file: uploadedFile, - id: uploadedFile.id, - url: `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`, - name: file.name, - collection_name: '', - status: 'uploaded', - error: '' - }; - files = [...files, fileItem]; + try { + const uploadedFile = await uploadFile(localStorage.token, file); - // TODO: Check if tools & functions have files support to skip this step to delegate file processing - // Default Upload to VectorDB - if ( - SUPPORTED_FILE_TYPE.includes(file['type']) || - SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1)) - ) { - processFileItem(fileItem); + if (uploadedFile) { + fileItem.status = 'uploaded'; + fileItem.file = uploadedFile; + fileItem.id = uploadedFile.id; + fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`; + + // TODO: Check if tools & functions have files support to skip this step to delegate file processing + // Default Upload to VectorDB + if ( + SUPPORTED_FILE_TYPE.includes(file['type']) || + SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1)) + ) { + processFileItem(fileItem); + } else { + toast.error( + $i18n.t(`Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.`, { + file_type: file['type'] + }) + ); + processFileItem(fileItem); + } } else { - toast.error( - $i18n.t(`Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.`, { - file_type: file['type'] - }) - ); - processFileItem(fileItem); + files = files.filter((item) => item.status !== null); } + } catch (e) { + toast.error(e); + files = files.filter((item) => item.status !== null); } }; @@ -162,7 +172,6 @@ // Remove the failed doc from the files array // files = files.filter((f) => f.id !== fileItem.id); toast.error(e); - fileItem.status = 'processed'; files = files; } @@ -545,6 +554,7 @@ { diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte index 3e9c755c0..011d77254 100644 --- a/src/lib/components/chat/Messages/ResponseMessage.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -253,7 +253,9 @@ for (const [idx, sentence] of sentences.entries()) { const res = await synthesizeOpenAISpeech( localStorage.token, - $settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice, + $settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice + ? $settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice + : $config?.audio?.tts?.voice, sentence ).catch((error) => { toast.error(error); diff --git a/src/lib/components/chat/Messages/UserMessage.svelte b/src/lib/components/chat/Messages/UserMessage.svelte index 748fcd61b..22b7081d7 100644 --- a/src/lib/components/chat/Messages/UserMessage.svelte +++ b/src/lib/components/chat/Messages/UserMessage.svelte @@ -104,6 +104,7 @@ url={file.url} name={file.name} type={file.type} + size={file?.size} colorClassName="bg-white dark:bg-gray-850 " /> {/if} diff --git a/src/lib/components/chat/ModelSelector/Selector.svelte b/src/lib/components/chat/ModelSelector/Selector.svelte index e443e1f13..d36f3aa72 100644 --- a/src/lib/components/chat/ModelSelector/Selector.svelte +++ b/src/lib/components/chat/ModelSelector/Selector.svelte @@ -1,6 +1,7 @@ @@ -79,6 +84,7 @@ }, tts: { voice: voice !== '' ? voice : undefined, + defaultVoice: $config?.audio?.tts?.voice ?? '', nonLocalVoices: $config.audio.tts.engine === '' ? nonLocalVoices : undefined } } @@ -195,7 +201,7 @@ {#each voices as voice} - {/each}
diff --git a/src/lib/components/chat/SettingsModal.svelte b/src/lib/components/chat/SettingsModal.svelte index 66f0efad5..450da97e1 100644 --- a/src/lib/components/chat/SettingsModal.svelte +++ b/src/lib/components/chat/SettingsModal.svelte @@ -15,7 +15,6 @@ import Chats from './Settings/Chats.svelte'; import User from '../icons/User.svelte'; import Personalization from './Settings/Personalization.svelte'; - import Valves from './Settings/Valves.svelte'; const i18n = getContext('i18n'); @@ -188,30 +187,6 @@
{$i18n.t('Audio')}
- -
-
+
{name}
-
+
{#if type === 'file'} {$i18n.t('File')} {:else if type === 'doc'} @@ -107,6 +122,9 @@ {:else} {type} {/if} + {#if size} + {formatSize(size)} + {/if}
diff --git a/src/lib/components/common/ImagePreview.svelte b/src/lib/components/common/ImagePreview.svelte index 16253b8a2..c1d1fef6b 100644 --- a/src/lib/components/common/ImagePreview.svelte +++ b/src/lib/components/common/ImagePreview.svelte @@ -7,6 +7,8 @@ let mounted = false; + let previewElement = null; + const downloadImage = (url, filename) => { fetch(url) .then((response) => response.blob()) @@ -34,14 +36,14 @@ mounted = true; }); - $: if (mounted) { - if (show) { - window.addEventListener('keydown', handleKeyDown); - document.body.style.overflow = 'hidden'; - } else { - window.removeEventListener('keydown', handleKeyDown); - document.body.style.overflow = 'unset'; - } + $: if (show && previewElement) { + document.body.appendChild(previewElement); + window.addEventListener('keydown', handleKeyDown); + document.body.style.overflow = 'hidden'; + } else if (previewElement) { + window.removeEventListener('keydown', handleKeyDown); + document.body.removeChild(previewElement); + document.body.style.overflow = 'unset'; } @@ -49,7 +51,8 @@
diff --git a/src/lib/components/common/SensitiveInput.svelte b/src/lib/components/common/SensitiveInput.svelte index a1d7b9248..76a626650 100644 --- a/src/lib/components/common/SensitiveInput.svelte +++ b/src/lib/components/common/SensitiveInput.svelte @@ -13,13 +13,13 @@