diff --git a/CHANGELOG.md b/CHANGELOG.md index 5db213e35..842276c28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ 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.4.5] - 2024-11-26 + +### Added + +- **🎨 Model Order/Defaults Reintroduced**: Brought back the ability to set model order and default models, now configurable via Admin Settings > Models > Configure (Gear Icon). + +### Fixed + +- **🔍 Query Generation Issue**: Resolved an error in web search query generation, enhancing search accuracy and ensuring smoother search workflows. +- **📏 Textarea Auto Height Bug**: Fixed a layout issue where textarea input height was shifting unpredictably, particularly when editing system prompts. +- **🔑 Ollama Authentication**: Corrected an issue with Ollama’s authorization headers, guaranteeing reliable authentication across all endpoints. +- **⚙️ Missing Min_P Save**: Resolved an issue where the 'min_p' parameter was not being saved in configurations. +- **🛠️ Tools Description**: Fixed a key issue that omitted tool descriptions in tools payload. + ## [0.4.4] - 2024-11-22 ### Added diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 8d84a23da..b1c7b56a3 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,25 +1,30 @@ # Contributor Covenant Code of Conduct ## Our Pledge + As members, contributors, and leaders of this community, we pledge to make participation in our open-source project a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We are committed to creating and maintaining an open, respectful, and professional environment where positive contributions and meaningful discussions can flourish. By participating in this project, you agree to uphold these values and align your behavior to the standards outlined in this Code of Conduct. ## Why These Standards Are Important -Open-source projects rely on a community of volunteers dedicating their time, expertise, and effort toward a shared goal. These projects are inherently collaborative but also fragile, as the success of the project depends on the goodwill, energy, and productivity of those involved. + +Open-source projects rely on a community of volunteers dedicating their time, expertise, and effort toward a shared goal. These projects are inherently collaborative but also fragile, as the success of the project depends on the goodwill, energy, and productivity of those involved. Maintaining a positive and respectful environment is essential to safeguarding the integrity of this project and protecting contributors' efforts. Behavior that disrupts this atmosphere—whether through hostility, entitlement, or unprofessional conduct—can severely harm the morale and productivity of the community. **Strict enforcement of these standards ensures a safe and supportive space for meaningful collaboration.** This is a community where **respect and professionalism are mandatory.** Violations of these standards will result in **zero tolerance** and immediate enforcement to prevent disruption and ensure the well-being of all participants. ## Our Standards + Examples of behavior that contribute to a positive and professional community include: + - **Respecting others.** Be considerate, listen actively, and engage with empathy toward others' viewpoints and experiences. -- **Constructive feedback.** Provide actionable, thoughtful, and respectful feedback that helps improve the project and encourages collaboration. Avoid unproductive negativity or hypercriticism. +- **Constructive feedback.** Provide actionable, thoughtful, and respectful feedback that helps improve the project and encourages collaboration. Avoid unproductive negativity or hypercriticism. - **Recognizing volunteer contributions.** Appreciate that contributors dedicate their free time and resources selflessly. Approach them with gratitude and patience. - **Focusing on shared goals.** Collaborate in ways that prioritize the health, success, and sustainability of the community over individual agendas. Examples of unacceptable behavior include: + - The use of discriminatory, demeaning, or sexualized language or behavior. - Personal attacks, derogatory comments, trolling, or inflammatory political or ideological arguments. - Harassment, intimidation, or any behavior intended to create a hostile, uncomfortable, or unsafe environment. @@ -29,32 +34,40 @@ Examples of unacceptable behavior include: - **Spamming and promotional exploitation.** Sharing irrelevant product promotions or self-promotion in the community is not allowed unless it directly contributes value to the discussion. ### Feedback and Community Engagement + - **Constructive feedback is encouraged, but hostile or entitled behavior will result in immediate action.** If you disagree with elements of the project, we encourage you to offer meaningful improvements or fork the project if necessary. Healthy discussions and technical disagreements are welcome only when handled with professionalism. - **Respect contributors' time and efforts.** No one is entitled to personalized or on-demand assistance. This is a community built on collaboration and shared effort; demanding or demeaning behavior undermines that trust and will not be allowed. ### Zero Tolerance: No Warnings, Immediate Action -This community operates under a **zero-tolerance policy.** Any behavior deemed unacceptable under this Code of Conduct will result in **immediate enforcement, without prior warning.** + +This community operates under a **zero-tolerance policy.** Any behavior deemed unacceptable under this Code of Conduct will result in **immediate enforcement, without prior warning.** We employ this approach to ensure that unproductive or disruptive behavior does not escalate further or cause unnecessary harm to other contributors. The standards are clear, and violations of any kind—whether mild or severe—will be addressed decisively to protect the community. ## Enforcement Responsibilities + Community leaders are responsible for upholding and enforcing these standards. They are empowered to take **immediate and appropriate action** to address any behaviors they deem unacceptable under this Code of Conduct. These actions are taken with the goal of protecting the community and preserving its safe, positive, and productive environment. ## Scope + This Code of Conduct applies to all community spaces, including forums, repositories, social media accounts, and in-person events. It also applies when an individual represents the community in public settings, such as conferences or official communications. Additionally, any behavior outside of these defined spaces that negatively impacts the community or its members may fall within the scope of this Code of Conduct. ## Reporting Violations + Instances of unacceptable behavior can be reported to the leadership team at **hello@openwebui.com**. Reports will be handled promptly, confidentially, and with consideration for the safety and well-being of the reporter. All community leaders are required to uphold confidentiality and impartiality when addressing reports of violations. ## Enforcement Guidelines + ### Ban + **Community Impact**: Community leaders will issue a ban to any participant whose behavior is deemed unacceptable according to this Code of Conduct. Bans are enforced immediately and without prior notice. A ban may be temporary or permanent, depending on the severity of the violation. This includes—but is not limited to—behavior such as: + - Harassment or abusive behavior toward contributors. - Persistent negativity or hostility that disrupts the collaborative environment. - Disrespectful, demanding, or aggressive interactions with others. @@ -65,6 +78,7 @@ A ban may be temporary or permanent, depending on the severity of the violation. This approach ensures that disruptive behaviors are addressed swiftly and decisively in order to maintain the integrity and productivity of the community. ## Why Zero Tolerance Is Necessary + Open-source projects thrive on collaboration, goodwill, and mutual respect. Toxic behaviors—such as entitlement, hostility, or persistent negativity—threaten not just individual contributors but the health of the project as a whole. Allowing such behaviors to persist robs contributors of their time, energy, and enthusiasm for the work they do. By enforcing a zero-tolerance policy, we ensure that the community remains a safe, welcoming space for all participants. These measures are not about harshness—they are about protecting contributors and fostering a productive environment where innovation can thrive. @@ -72,13 +86,14 @@ By enforcing a zero-tolerance policy, we ensure that the community remains a saf Our expectations are clear, and our enforcement reflects our commitment to this project's long-term success. ## Attribution + This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at -https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. -Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). +Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). -[homepage]: https://www.contributor-covenant.org +[homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. \ No newline at end of file +https://www.contributor-covenant.org/translations. diff --git a/backend/open_webui/apps/audio/main.py b/backend/open_webui/apps/audio/main.py index 384bb3cd4..5c24c2633 100644 --- a/backend/open_webui/apps/audio/main.py +++ b/backend/open_webui/apps/audio/main.py @@ -8,6 +8,8 @@ from pathlib import Path from pydub import AudioSegment from pydub.silence import split_on_silence +import aiohttp +import aiofiles import requests from open_webui.config import ( AUDIO_STT_ENGINE, @@ -292,46 +294,39 @@ async def speech(request: Request, user=Depends(get_verified_user)): except Exception: pass - r = None try: - r = requests.post( - url=f"{app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech", - data=body, - headers=headers, - stream=True, - ) + async with aiohttp.ClientSession() as session: + async with session.post( + url=f"{app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech", + data=body, + headers=headers, + ) as r: + r.raise_for_status() + async with aiofiles.open(file_path, "wb") as f: + await f.write(await r.read()) - r.raise_for_status() + async with aiofiles.open(file_body_path, "w") as f: + await f.write(json.dumps(json.loads(body.decode("utf-8")))) - # Save the streaming content to a file - with open(file_path, "wb") as f: - for chunk in r.iter_content(chunk_size=8192): - f.write(chunk) - - with open(file_body_path, "w") as f: - json.dump(json.loads(body.decode("utf-8")), f) - - # Return the saved file return FileResponse(file_path) except Exception as e: log.exception(e) error_detail = "Open WebUI: Server Connection Error" - if r is not None: - try: - res = r.json() + try: + if r.status != 200: + res = await r.json() if "error" in res: error_detail = f"External: {res['error']['message']}" - except Exception: - error_detail = f"External: {e}" + except Exception: + error_detail = f"External: {e}" raise HTTPException( - status_code=r.status_code if r != None else 500, + status_code=getattr(r, "status", 500), detail=error_detail, ) elif app.state.config.TTS_ENGINE == "elevenlabs": - payload = None try: payload = json.loads(body.decode("utf-8")) except Exception as e: @@ -339,7 +334,6 @@ async def speech(request: Request, user=Depends(get_verified_user)): raise HTTPException(status_code=400, detail="Invalid JSON payload") voice_id = payload.get("voice", "") - if voice_id not in get_available_voices(): raise HTTPException( status_code=400, @@ -347,13 +341,11 @@ async def speech(request: Request, user=Depends(get_verified_user)): ) url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}" - headers = { "Accept": "audio/mpeg", "Content-Type": "application/json", "xi-api-key": app.state.config.TTS_API_KEY, } - data = { "text": payload["input"], "model_id": app.state.config.TTS_MODEL, @@ -361,39 +353,34 @@ async def speech(request: Request, user=Depends(get_verified_user)): } try: - r = requests.post(url, json=data, headers=headers) + async with aiohttp.ClientSession() as session: + async with session.post(url, json=data, headers=headers) as r: + r.raise_for_status() + async with aiofiles.open(file_path, "wb") as f: + await f.write(await r.read()) - r.raise_for_status() + async with aiofiles.open(file_body_path, "w") as f: + await f.write(json.dumps(json.loads(body.decode("utf-8")))) - # Save the streaming content to a file - with open(file_path, "wb") as f: - for chunk in r.iter_content(chunk_size=8192): - f.write(chunk) - - with open(file_body_path, "w") as f: - json.dump(json.loads(body.decode("utf-8")), f) - - # Return the saved file return FileResponse(file_path) except Exception as e: log.exception(e) error_detail = "Open WebUI: Server Connection Error" - if r is not None: - try: - res = r.json() + try: + if r.status != 200: + res = await r.json() if "error" in res: error_detail = f"External: {res['error']['message']}" - except Exception: - error_detail = f"External: {e}" + except Exception: + error_detail = f"External: {e}" raise HTTPException( - status_code=r.status_code if r != None else 500, + status_code=getattr(r, "status", 500), detail=error_detail, ) elif app.state.config.TTS_ENGINE == "azure": - payload = None try: payload = json.loads(body.decode("utf-8")) except Exception as e: @@ -416,17 +403,20 @@ async def speech(request: Request, user=Depends(get_verified_user)): {payload["input"]} """ - response = requests.post(url, headers=headers, data=data) - - if response.status_code == 200: - with open(file_path, "wb") as f: - f.write(response.content) - return FileResponse(file_path) - else: - log.error(f"Error synthesizing speech - {response.reason}") - raise HTTPException( - status_code=500, detail=f"Error synthesizing speech - {response.reason}" - ) + try: + async with aiohttp.ClientSession() as session: + async with session.post(url, headers=headers, data=data) as response: + if response.status == 200: + async with aiofiles.open(file_path, "wb") as f: + await f.write(await response.read()) + return FileResponse(file_path) + else: + error_msg = f"Error synthesizing speech - {response.reason}" + log.error(error_msg) + raise HTTPException(status_code=500, detail=error_msg) + except Exception as e: + log.exception(e) + raise HTTPException(status_code=500, detail=str(e)) elif app.state.config.TTS_ENGINE == "transformers": payload = None try: diff --git a/backend/open_webui/apps/ollama/main.py b/backend/open_webui/apps/ollama/main.py index b44f68017..0ac1f0401 100644 --- a/backend/open_webui/apps/ollama/main.py +++ b/backend/open_webui/apps/ollama/main.py @@ -195,7 +195,10 @@ async def post_streaming_url( trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) ) - api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {}) + parsed_url = urlparse(url) + base_url = f"{parsed_url.scheme}://{parsed_url.netloc}" + + api_config = app.state.config.OLLAMA_API_CONFIGS.get(base_url, {}) key = api_config.get("key", None) headers = {"Content-Type": "application/json"} @@ -210,13 +213,13 @@ async def post_streaming_url( r.raise_for_status() if stream: - headers = dict(r.headers) + response_headers = dict(r.headers) if content_type: - headers["Content-Type"] = content_type + response_headers["Content-Type"] = content_type return StreamingResponse( r.content, status_code=r.status, - headers=headers, + headers=response_headers, background=BackgroundTask( cleanup_response, response=r, session=session ), @@ -324,7 +327,10 @@ async def get_ollama_tags( else: url = app.state.config.OLLAMA_BASE_URLS[url_idx] - api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {}) + parsed_url = urlparse(url) + base_url = f"{parsed_url.scheme}://{parsed_url.netloc}" + + api_config = app.state.config.OLLAMA_API_CONFIGS.get(base_url, {}) key = api_config.get("key", None) headers = {} @@ -525,7 +531,10 @@ async def copy_model( url = app.state.config.OLLAMA_BASE_URLS[url_idx] log.info(f"url: {url}") - api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {}) + parsed_url = urlparse(url) + base_url = f"{parsed_url.scheme}://{parsed_url.netloc}" + + api_config = app.state.config.OLLAMA_API_CONFIGS.get(base_url, {}) key = api_config.get("key", None) headers = {"Content-Type": "application/json"} @@ -584,7 +593,10 @@ async def delete_model( url = app.state.config.OLLAMA_BASE_URLS[url_idx] log.info(f"url: {url}") - api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {}) + parsed_url = urlparse(url) + base_url = f"{parsed_url.scheme}://{parsed_url.netloc}" + + api_config = app.state.config.OLLAMA_API_CONFIGS.get(base_url, {}) key = api_config.get("key", None) headers = {"Content-Type": "application/json"} @@ -635,7 +647,10 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_verified_us url = app.state.config.OLLAMA_BASE_URLS[url_idx] log.info(f"url: {url}") - api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {}) + parsed_url = urlparse(url) + base_url = f"{parsed_url.scheme}://{parsed_url.netloc}" + + api_config = app.state.config.OLLAMA_API_CONFIGS.get(base_url, {}) key = api_config.get("key", None) headers = {"Content-Type": "application/json"} @@ -730,7 +745,10 @@ async def generate_ollama_embeddings( url = app.state.config.OLLAMA_BASE_URLS[url_idx] log.info(f"url: {url}") - api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {}) + parsed_url = urlparse(url) + base_url = f"{parsed_url.scheme}://{parsed_url.netloc}" + + api_config = app.state.config.OLLAMA_API_CONFIGS.get(base_url, {}) key = api_config.get("key", None) headers = {"Content-Type": "application/json"} @@ -797,7 +815,10 @@ async def generate_ollama_batch_embeddings( url = app.state.config.OLLAMA_BASE_URLS[url_idx] log.info(f"url: {url}") - api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {}) + parsed_url = urlparse(url) + base_url = f"{parsed_url.scheme}://{parsed_url.netloc}" + + api_config = app.state.config.OLLAMA_API_CONFIGS.get(base_url, {}) key = api_config.get("key", None) headers = {"Content-Type": "application/json"} @@ -974,7 +995,10 @@ async def generate_chat_completion( log.info(f"url: {url}") log.debug(f"generate_chat_completion() - 2.payload = {payload}") - api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {}) + parsed_url = urlparse(url) + base_url = f"{parsed_url.scheme}://{parsed_url.netloc}" + + api_config = app.state.config.OLLAMA_API_CONFIGS.get(base_url, {}) prefix_id = api_config.get("prefix_id", None) if prefix_id: payload["model"] = payload["model"].replace(f"{prefix_id}.", "") diff --git a/backend/open_webui/apps/openai/main.py b/backend/open_webui/apps/openai/main.py index 6d6ac50c6..31c36a8a1 100644 --- a/backend/open_webui/apps/openai/main.py +++ b/backend/open_webui/apps/openai/main.py @@ -585,8 +585,6 @@ async def generate_chat_completion( # Convert the modified body back to JSON payload = json.dumps(payload) - log.debug(payload) - headers = {} headers["Authorization"] = f"Bearer {key}" headers["Content-Type"] = "application/json" diff --git a/backend/open_webui/apps/retrieval/utils.py b/backend/open_webui/apps/retrieval/utils.py index 420ed7bf4..35159f80d 100644 --- a/backend/open_webui/apps/retrieval/utils.py +++ b/backend/open_webui/apps/retrieval/utils.py @@ -15,8 +15,6 @@ from open_webui.apps.retrieval.vector.connector import VECTOR_DB_CLIENT from open_webui.utils.misc import get_last_user_message from open_webui.env import SRC_LOG_LEVELS -from open_webui.config import DEFAULT_RAG_TEMPLATE - log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["RAG"]) @@ -238,44 +236,6 @@ def query_collection_with_hybrid_search( return merge_and_sort_query_results(results, k=k, reverse=True) -def rag_template(template: str, context: str, query: str): - if template == "": - template = DEFAULT_RAG_TEMPLATE - - if "[context]" not in template and "{{CONTEXT}}" not in template: - log.debug( - "WARNING: The RAG template does not contain the '[context]' or '{{CONTEXT}}' placeholder." - ) - - if "" in context and "" in context: - log.debug( - "WARNING: Potential prompt injection attack: the RAG " - "context contains '' and ''. This might be " - "nothing, or the user might be trying to hack something." - ) - - query_placeholders = [] - if "[query]" in context: - query_placeholder = "{{QUERY" + str(uuid.uuid4()) + "}}" - template = template.replace("[query]", query_placeholder) - query_placeholders.append(query_placeholder) - - if "{{QUERY}}" in context: - query_placeholder = "{{QUERY" + str(uuid.uuid4()) + "}}" - template = template.replace("{{QUERY}}", query_placeholder) - query_placeholders.append(query_placeholder) - - template = template.replace("[context]", context) - template = template.replace("{{CONTEXT}}", context) - template = template.replace("[query]", query) - template = template.replace("{{QUERY}}", query) - - for query_placeholder in query_placeholders: - template = template.replace(query_placeholder, query) - - return template - - def get_embedding_function( embedding_engine, embedding_model, diff --git a/backend/open_webui/apps/webui/main.py b/backend/open_webui/apps/webui/main.py index ce4945b69..054c6280e 100644 --- a/backend/open_webui/apps/webui/main.py +++ b/backend/open_webui/apps/webui/main.py @@ -31,6 +31,7 @@ from open_webui.config import ( DEFAULT_MODELS, DEFAULT_PROMPT_SUGGESTIONS, DEFAULT_USER_ROLE, + MODEL_ORDER_LIST, ENABLE_COMMUNITY_SHARING, ENABLE_LOGIN_FORM, ENABLE_MESSAGE_RATING, @@ -68,6 +69,7 @@ from open_webui.config import ( ) from open_webui.env import ( ENV, + SRC_LOG_LEVELS, WEBUI_AUTH_TRUSTED_EMAIL_HEADER, WEBUI_AUTH_TRUSTED_NAME_HEADER, ) @@ -94,6 +96,7 @@ app = FastAPI( ) log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MAIN"]) app.state.config = AppConfig() @@ -118,6 +121,7 @@ app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE app.state.config.USER_PERMISSIONS = USER_PERMISSIONS app.state.config.WEBHOOK_URL = WEBHOOK_URL app.state.config.BANNERS = WEBUI_BANNERS +app.state.config.MODEL_ORDER_LIST = MODEL_ORDER_LIST app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING app.state.config.ENABLE_MESSAGE_RATING = ENABLE_MESSAGE_RATING @@ -270,7 +274,9 @@ async def get_pipe_models(): log.exception(e) sub_pipes = [] - print(sub_pipes) + log.debug( + f"get_pipe_models: function '{pipe.id}' is a manifold of {sub_pipes}" + ) for p in sub_pipes: sub_pipe_id = f'{pipe.id}.{p["id"]}' @@ -280,6 +286,7 @@ async def get_pipe_models(): sub_pipe_name = f"{function_module.name}{sub_pipe_name}" pipe_flag = {"type": pipe.type} + pipe_models.append( { "id": sub_pipe_id, @@ -293,6 +300,10 @@ async def get_pipe_models(): else: pipe_flag = {"type": "pipe"} + log.debug( + f"get_pipe_models: function '{pipe.id}' is a single pipe {{ 'id': {pipe.id}, 'name': {pipe.name} }}" + ) + pipe_models.append( { "id": pipe.id, @@ -346,7 +357,7 @@ 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 @@ -453,7 +464,7 @@ async def generate_function_chat_completion(form_data, user, models: dict = {}): return except Exception as e: - print(f"Error: {e}") + log.error(f"Error: {e}") yield f"data: {json.dumps({'error': {'detail':str(e)}})}\n\n" return @@ -483,7 +494,7 @@ async def generate_function_chat_completion(form_data, user, models: dict = {}): res = await execute_pipe(pipe, params) except Exception as e: - print(f"Error: {e}") + log.error(f"Error: {e}") return {"error": {"detail": str(e)}} if isinstance(res, StreamingResponse) or isinstance(res, dict): diff --git a/backend/open_webui/apps/webui/routers/configs.py b/backend/open_webui/apps/webui/routers/configs.py index 1c30b0b3b..b19fc1745 100644 --- a/backend/open_webui/apps/webui/routers/configs.py +++ b/backend/open_webui/apps/webui/routers/configs.py @@ -34,8 +34,32 @@ async def export_config(user=Depends(get_admin_user)): return get_config() -class SetDefaultModelsForm(BaseModel): - models: str +############################ +# SetDefaultModels +############################ +class ModelsConfigForm(BaseModel): + DEFAULT_MODELS: str + MODEL_ORDER_LIST: list[str] + + +@router.get("/models", response_model=ModelsConfigForm) +async def get_models_config(request: Request, user=Depends(get_admin_user)): + return { + "DEFAULT_MODELS": request.app.state.config.DEFAULT_MODELS, + "MODEL_ORDER_LIST": request.app.state.config.MODEL_ORDER_LIST, + } + + +@router.post("/models", response_model=ModelsConfigForm) +async def set_models_config( + request: Request, form_data: ModelsConfigForm, user=Depends(get_admin_user) +): + request.app.state.config.DEFAULT_MODELS = form_data.DEFAULT_MODELS + request.app.state.config.MODEL_ORDER_LIST = form_data.MODEL_ORDER_LIST + return { + "DEFAULT_MODELS": request.app.state.config.DEFAULT_MODELS, + "MODEL_ORDER_LIST": request.app.state.config.MODEL_ORDER_LIST, + } class PromptSuggestion(BaseModel): @@ -47,21 +71,8 @@ class SetDefaultSuggestionsForm(BaseModel): suggestions: list[PromptSuggestion] -############################ -# SetDefaultModels -############################ - - -@router.post("/default/models", response_model=str) -async def set_global_default_models( - request: Request, form_data: SetDefaultModelsForm, user=Depends(get_admin_user) -): - request.app.state.config.DEFAULT_MODELS = form_data.models - return request.app.state.config.DEFAULT_MODELS - - -@router.post("/default/suggestions", response_model=list[PromptSuggestion]) -async def set_global_default_suggestions( +@router.post("/suggestions", response_model=list[PromptSuggestion]) +async def set_default_suggestions( request: Request, form_data: SetDefaultSuggestionsForm, user=Depends(get_admin_user), diff --git a/backend/open_webui/apps/webui/utils.py b/backend/open_webui/apps/webui/utils.py index 6bfddd072..054158b3e 100644 --- a/backend/open_webui/apps/webui/utils.py +++ b/backend/open_webui/apps/webui/utils.py @@ -5,10 +5,15 @@ import sys from importlib import util import types import tempfile +import logging +from open_webui.env import SRC_LOG_LEVELS from open_webui.apps.webui.models.functions import Functions from open_webui.apps.webui.models.tools import Tools +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MAIN"]) + def extract_frontmatter(content): """ @@ -95,7 +100,7 @@ def load_tools_module_by_id(toolkit_id, content=None): # Executing the modified content in the created module's namespace exec(content, module.__dict__) frontmatter = extract_frontmatter(content) - print(f"Loaded module: {module.__name__}") + log.info(f"Loaded module: {module.__name__}") # Create and return the object if the class 'Tools' is found in the module if hasattr(module, "Tools"): @@ -103,7 +108,7 @@ def load_tools_module_by_id(toolkit_id, content=None): else: raise Exception("No Tools class found in the module") except Exception as e: - print(f"Error loading module: {toolkit_id}: {e}") + log.error(f"Error loading module: {toolkit_id}: {e}") del sys.modules[module_name] # Clean up raise e finally: @@ -139,7 +144,7 @@ def load_function_module_by_id(function_id, content=None): # Execute the modified content in the created module's namespace exec(content, module.__dict__) frontmatter = extract_frontmatter(content) - print(f"Loaded module: {module.__name__}") + log.info(f"Loaded module: {module.__name__}") # Create appropriate object based on available class type in the module if hasattr(module, "Pipe"): @@ -151,7 +156,7 @@ def load_function_module_by_id(function_id, content=None): else: raise Exception("No Function class found in the module") except Exception as e: - print(f"Error loading module: {function_id}: {e}") + log.error(f"Error loading module: {function_id}: {e}") del sys.modules[module_name] # Cleanup by removing the module in case of error Functions.update_function_by_id(function_id, {"is_active": False}) @@ -164,7 +169,7 @@ def install_frontmatter_requirements(requirements): if requirements: req_list = [req.strip() for req in requirements.split(",")] for req in req_list: - print(f"Installing requirement: {req}") + log.info(f"Installing requirement: {req}") subprocess.check_call([sys.executable, "-m", "pip", "install", req]) else: - print("No requirements found in frontmatter.") + log.info("No requirements found in frontmatter.") diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 3b12b2f94..3c1ee798d 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -740,6 +740,12 @@ DEFAULT_PROMPT_SUGGESTIONS = PersistentConfig( ], ) +MODEL_ORDER_LIST = PersistentConfig( + "MODEL_ORDER_LIST", + "ui.model_order_list", + [], +) + DEFAULT_USER_ROLE = PersistentConfig( "DEFAULT_USER_ROLE", "ui.default_user_role", @@ -969,19 +975,20 @@ QUERY_GENERATION_PROMPT_TEMPLATE = PersistentConfig( ) DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE = """### Task: -Based on the chat history, determine whether a search is necessary, and if so, generate a 1-3 broad search queries to retrieve comprehensive and updated information. If no search is required, return an empty list. +Analyze the chat history to determine the necessity of generating search queries. By default, **prioritize generating 1-3 broad and relevant search queries** unless it is absolutely certain that no additional information is required. The aim is to retrieve comprehensive, updated, and valuable information even with minimal uncertainty. If no search is unequivocally needed, return an empty list. ### Guidelines: -- Respond exclusively with a JSON object. -- If a search query is needed, return an object like: { "queries": ["query1", "query2"] } where each query is distinct and concise. -- If no search query is necessary, output should be: { "queries": [] } -- Default to suggesting a search query to ensure accurate and updated information, unless it is definitively clear no search is required. -- Be concise, focusing strictly on composing search queries with no additional commentary or text. -- When in doubt, prefer to suggest a search for comprehensiveness. -- Today's date is: {{CURRENT_DATE}} +- Respond **EXCLUSIVELY** with a JSON object. Any form of extra commentary, explanation, or additional text is strictly prohibited. +- When generating search queries, respond in the format: { "queries": ["query1", "query2"] }, ensuring each query is distinct, concise, and relevant to the topic. +- If and only if it is entirely certain that no useful results can be retrieved by a search, return: { "queries": [] }. +- Err on the side of suggesting search queries if there is **any chance** they might provide useful or updated information. +- Be concise and focused on composing high-quality search queries, avoiding unnecessary elaboration, commentary, or assumptions. +- Assume today's date is: {{CURRENT_DATE}}. +- Always prioritize providing actionable and broad queries that maximize informational coverage. ### Output: -JSON format: { +Strictly return in JSON format: +{ "queries": ["query1", "query2"] } diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index cfa13e0a5..0dca21b08 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -49,7 +49,9 @@ from open_webui.apps.openai.main import ( get_all_models_responses as get_openai_models_responses, ) from open_webui.apps.retrieval.main import app as retrieval_app -from open_webui.apps.retrieval.utils import get_sources_from_files, rag_template +from open_webui.apps.retrieval.utils import get_sources_from_files + + from open_webui.apps.socket.main import ( app as socket_app, periodic_usage_pool_cleanup, @@ -122,11 +124,12 @@ from open_webui.utils.response import ( ) from open_webui.utils.security_headers import SecurityHeadersMiddleware from open_webui.utils.task import ( - moa_response_generation_template, - tags_generation_template, - query_generation_template, - emoji_generation_template, + rag_template, title_generation_template, + query_generation_template, + tags_generation_template, + emoji_generation_template, + moa_response_generation_template, tools_function_calling_generation_template, ) from open_webui.utils.tools import get_tools @@ -539,8 +542,6 @@ async def chat_completion_files_handler( if len(queries) == 0: queries = [get_last_user_message(body["messages"])] - print(f"{queries=}") - sources = get_sources_from_files( files=files, queries=queries, @@ -970,7 +971,7 @@ app.add_middleware(SecurityHeadersMiddleware) @app.middleware("http") async def commit_session_after_request(request: Request, call_next): response = await call_next(request) - log.debug("Commit session after request") + # log.debug("Commit session after request") Session.commit() return response @@ -1177,6 +1178,8 @@ async def get_all_models(): model["actions"].extend( get_action_items_from_module(action_function, function_module) ) + log.debug(f"get_all_models() returned {len(models)} models") + return models @@ -1191,6 +1194,14 @@ async def get_models(user=Depends(get_verified_user)): if "pipeline" not in model or model["pipeline"].get("type", None) != "filter" ] + model_order_list = webui_app.state.config.MODEL_ORDER_LIST + if model_order_list: + model_order_dict = {model_id: i for i, model_id in enumerate(model_order_list)} + # Sort models by order list priority, with fallback for those not in the list + models.sort( + key=lambda x: (model_order_dict.get(x["id"], float("inf")), x["name"]) + ) + # Filter out models that the user does not have access to if user.role == "user": filtered_models = [] @@ -1214,6 +1225,10 @@ async def get_models(user=Depends(get_verified_user)): filtered_models.append(model) models = filtered_models + log.debug( + f"/api/models returned filtered models accessible to the user: {json.dumps([model['id'] for model in models])}" + ) + return {"data": models} @@ -1704,7 +1719,6 @@ async def update_task_config(form_data: TaskConfigForm, user=Depends(get_admin_u @app.post("/api/task/title/completions") async def generate_title(form_data: dict, user=Depends(get_verified_user)): - print("generate_title") model_list = await get_all_models() models = {model["id"]: model for model in model_list} @@ -1725,9 +1739,9 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)): models, ) - print(task_model_id) - - model = models[task_model_id] + log.debug( + f"generating chat title using model {task_model_id} for user {user.email} " + ) if app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE != "": template = app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE @@ -1766,10 +1780,12 @@ Artificial Intelligence in Healthcare "max_completion_tokens": 50, } ), - "chat_id": form_data.get("chat_id", None), - "metadata": {"task": str(TASKS.TITLE_GENERATION), "task_body": form_data}, + "metadata": { + "task": str(TASKS.TITLE_GENERATION), + "task_body": form_data, + "chat_id": form_data.get("chat_id", None), + }, } - log.debug(payload) # Handle pipeline filters try: @@ -1793,7 +1809,7 @@ Artificial Intelligence in Healthcare @app.post("/api/task/tags/completions") async def generate_chat_tags(form_data: dict, user=Depends(get_verified_user)): - print("generate_chat_tags") + if not app.state.config.ENABLE_TAGS_GENERATION: return JSONResponse( status_code=status.HTTP_200_OK, @@ -1818,7 +1834,10 @@ async def generate_chat_tags(form_data: dict, user=Depends(get_verified_user)): app.state.config.TASK_MODEL_EXTERNAL, models, ) - print(task_model_id) + + log.debug( + f"generating chat tags using model {task_model_id} for user {user.email} " + ) if app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE != "": template = app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE @@ -1849,9 +1868,12 @@ JSON format: { "tags": ["tag1", "tag2", "tag3"] } "model": task_model_id, "messages": [{"role": "user", "content": content}], "stream": False, - "metadata": {"task": str(TASKS.TAGS_GENERATION), "task_body": form_data}, + "metadata": { + "task": str(TASKS.TAGS_GENERATION), + "task_body": form_data, + "chat_id": form_data.get("chat_id", None), + }, } - log.debug(payload) # Handle pipeline filters try: @@ -1875,7 +1897,7 @@ JSON format: { "tags": ["tag1", "tag2", "tag3"] } @app.post("/api/task/queries/completions") async def generate_queries(form_data: dict, user=Depends(get_verified_user)): - print("generate_queries") + type = form_data.get("type") if type == "web_search": if not app.state.config.ENABLE_SEARCH_QUERY_GENERATION: @@ -1908,9 +1930,10 @@ async def generate_queries(form_data: dict, user=Depends(get_verified_user)): app.state.config.TASK_MODEL_EXTERNAL, models, ) - print(task_model_id) - model = models[task_model_id] + log.debug( + f"generating {type} queries using model {task_model_id} for user {user.email}" + ) if app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE != "": template = app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE @@ -1925,9 +1948,12 @@ async def generate_queries(form_data: dict, user=Depends(get_verified_user)): "model": task_model_id, "messages": [{"role": "user", "content": content}], "stream": False, - "metadata": {"task": str(TASKS.QUERY_GENERATION), "task_body": form_data}, + "metadata": { + "task": str(TASKS.QUERY_GENERATION), + "task_body": form_data, + "chat_id": form_data.get("chat_id", None), + }, } - log.debug(payload) # Handle pipeline filters try: @@ -1951,7 +1977,6 @@ async def generate_queries(form_data: dict, user=Depends(get_verified_user)): @app.post("/api/task/emoji/completions") async def generate_emoji(form_data: dict, user=Depends(get_verified_user)): - print("generate_emoji") model_list = await get_all_models() models = {model["id"]: model for model in model_list} @@ -1971,9 +1996,8 @@ async def generate_emoji(form_data: dict, user=Depends(get_verified_user)): app.state.config.TASK_MODEL_EXTERNAL, models, ) - print(task_model_id) - model = models[task_model_id] + log.debug(f"generating emoji using model {task_model_id} for user {user.email} ") template = ''' Your task is to reflect the speaker's likely facial expression through a fitting emoji. Interpret emotions from the message and reflect their facial expression using fitting, diverse emojis (e.g., 😊, 😢, 😡, 😱). @@ -2003,7 +2027,6 @@ Message: """{{prompt}}""" "chat_id": form_data.get("chat_id", None), "metadata": {"task": str(TASKS.EMOJI_GENERATION), "task_body": form_data}, } - log.debug(payload) # Handle pipeline filters try: @@ -2027,7 +2050,6 @@ Message: """{{prompt}}""" @app.post("/api/task/moa/completions") async def generate_moa_response(form_data: dict, user=Depends(get_verified_user)): - print("generate_moa_response") model_list = await get_all_models() models = {model["id"]: model for model in model_list} @@ -2047,9 +2069,8 @@ async def generate_moa_response(form_data: dict, user=Depends(get_verified_user) app.state.config.TASK_MODEL_EXTERNAL, models, ) - print(task_model_id) - model = models[task_model_id] + log.debug(f"generating MOA model {task_model_id} for user {user.email} ") template = """You have been provided with a set of responses from various models to the latest user query: "{{prompt}}" @@ -2073,7 +2094,6 @@ Responses from models: {{responses}}""" "task_body": form_data, }, } - log.debug(payload) try: payload = filter_pipeline(payload, user, models) @@ -2108,7 +2128,7 @@ Responses from models: {{responses}}""" async def get_pipelines_list(user=Depends(get_admin_user)): responses = await get_openai_models_responses() - print(responses) + log.debug(f"get_pipelines_list: get_openai_models_responses returned {responses}") urlIdxs = [ idx for idx, response in enumerate(responses) diff --git a/backend/open_webui/utils/task.py b/backend/open_webui/utils/task.py index 28b07da37..b6d0d3bce 100644 --- a/backend/open_webui/utils/task.py +++ b/backend/open_webui/utils/task.py @@ -1,11 +1,20 @@ +import logging import math import re from datetime import datetime from typing import Optional +import uuid from open_webui.utils.misc import get_last_user_message, get_messages_content +from open_webui.env import SRC_LOG_LEVELS +from open_webui.config import DEFAULT_RAG_TEMPLATE + + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["RAG"]) + def prompt_template( template: str, user_name: Optional[str] = None, user_location: Optional[str] = None @@ -110,6 +119,44 @@ def replace_messages_variable(template: str, messages: list[str]) -> str: # {{prompt:middletruncate:8000}} +def rag_template(template: str, context: str, query: str): + if template == "": + template = DEFAULT_RAG_TEMPLATE + + if "[context]" not in template and "{{CONTEXT}}" not in template: + log.debug( + "WARNING: The RAG template does not contain the '[context]' or '{{CONTEXT}}' placeholder." + ) + + if "" in context and "" in context: + log.debug( + "WARNING: Potential prompt injection attack: the RAG " + "context contains '' and ''. This might be " + "nothing, or the user might be trying to hack something." + ) + + query_placeholders = [] + if "[query]" in context: + query_placeholder = "{{QUERY" + str(uuid.uuid4()) + "}}" + template = template.replace("[query]", query_placeholder) + query_placeholders.append(query_placeholder) + + if "{{QUERY}}" in context: + query_placeholder = "{{QUERY" + str(uuid.uuid4()) + "}}" + template = template.replace("{{QUERY}}", query_placeholder) + query_placeholders.append(query_placeholder) + + template = template.replace("[context]", context) + template = template.replace("{{CONTEXT}}", context) + template = template.replace("[query]", query) + template = template.replace("{{QUERY}}", query) + + for query_placeholder in query_placeholders: + template = template.replace(query_placeholder, query) + + return template + + def title_generation_template( template: str, messages: list[dict], user: Optional[dict] = None ) -> str: diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py index 3cdcf15bf..60a9f942f 100644 --- a/backend/open_webui/utils/tools.py +++ b/backend/open_webui/utils/tools.py @@ -90,6 +90,32 @@ def get_tools( return tools_dict +def parse_description(docstring: str | None) -> str: + """ + Parse a function's docstring to extract the description. + + Args: + docstring (str): The docstring to parse. + + Returns: + str: The description. + """ + + if not docstring: + return "" + + lines = [line.strip() for line in docstring.strip().split("\n")] + description_lines: list[str] = [] + + for line in lines: + if re.match(r":param", line) or re.match(r":return", line): + break + + description_lines.append(line) + + return "\n".join(description_lines) + + def parse_docstring(docstring): """ Parse a function's docstring to extract parameter descriptions in reST format. @@ -138,6 +164,8 @@ def function_to_pydantic_model(func: Callable) -> type[BaseModel]: docstring = func.__doc__ descriptions = parse_docstring(docstring) + tool_description = parse_description(docstring) + field_defs = {} for name, param in parameters.items(): type_hint = type_hints.get(name, Any) @@ -148,7 +176,10 @@ def function_to_pydantic_model(func: Callable) -> type[BaseModel]: continue field_defs[name] = type_hint, Field(default_value, description=description) - return create_model(func.__name__, **field_defs) + model = create_model(func.__name__, **field_defs) + model.__doc__ = tool_description + + return model def get_callable_attributes(tool: object) -> list[Callable]: diff --git a/backend/requirements.txt b/backend/requirements.txt index 258f69e25..c83e6b3b7 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -14,6 +14,7 @@ requests==2.32.3 aiohttp==3.10.8 async-timeout aiocache +aiofiles sqlalchemy==2.0.32 alembic==1.13.2 diff --git a/package-lock.json b/package-lock.json index 496e790d4..6b899042c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.4.4", + "version": "0.4.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.4.4", + "version": "0.4.5", "dependencies": { "@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-python": "^6.1.6", diff --git a/package.json b/package.json index 8f4bc6ae0..0db0d88f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.4.4", + "version": "0.4.5", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", diff --git a/pyproject.toml b/pyproject.toml index 9a1c2bb03..0dc8e856d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ dependencies = [ "aiohttp==3.10.8", "async-timeout", "aiocache", + "aiofiles", "sqlalchemy==2.0.32", "alembic==1.13.2", diff --git a/src/app.css b/src/app.css index 3974974bc..659498add 100644 --- a/src/app.css +++ b/src/app.css @@ -231,6 +231,15 @@ input[type='number'] { @apply dark:bg-gray-800 bg-gray-100; } +.tiptap p code { + color: #eb5757; + border-width: 0px; + padding: 3px 8px; + font-size: 0.8em; + font-weight: 600; + @apply rounded-md dark:bg-gray-800 bg-gray-100 mx-0.5; +} + /* Code styling */ .hljs-comment, .hljs-quote { diff --git a/src/lib/apis/configs/index.ts b/src/lib/apis/configs/index.ts index 0c4de6ad6..e9faf346b 100644 --- a/src/lib/apis/configs/index.ts +++ b/src/lib/apis/configs/index.ts @@ -58,17 +58,44 @@ export const exportConfig = async (token: string) => { return res; }; -export const setDefaultModels = async (token: string, models: string) => { +export const getModelsConfig = async (token: string) => { let error = null; - const res = await fetch(`${WEBUI_API_BASE_URL}/configs/default/models`, { + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/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) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const setModelsConfig = async (token: string, config: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/models`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ - models: models + ...config }) }) .then(async (res) => { @@ -91,7 +118,7 @@ export const setDefaultModels = async (token: string, models: string) => { export const setDefaultPromptSuggestions = async (token: string, promptSuggestions: string) => { let error = null; - const res = await fetch(`${WEBUI_API_BASE_URL}/configs/default/suggestions`, { + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/suggestions`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts index 699980a5e..a33610c31 100644 --- a/src/lib/apis/index.ts +++ b/src/lib/apis/index.ts @@ -25,26 +25,6 @@ export const getModels = async (token: string = '', base: boolean = false) => { } let models = res?.data ?? []; - models = models - .filter((models) => models) - // Sort the models - .sort((a, b) => { - // Compare case-insensitively by name for models without position property - const lowerA = a.name.toLowerCase(); - const lowerB = b.name.toLowerCase(); - - if (lowerA < lowerB) return -1; - if (lowerA > lowerB) return 1; - - // If same case-insensitively, sort by original strings, - // lowercase will come before uppercase due to ASCII values - if (a.name < b.name) return -1; - if (a.name > b.name) return 1; - - return 0; // They are equal - }); - - console.log(models); return models; }; @@ -391,16 +371,13 @@ export const generateQueries = async ( // Step 1: Safely extract the response string const response = res?.choices[0]?.message?.content ?? ''; - // Step 2: Attempt to fix common JSON format issues like single quotes - const sanitizedResponse = response.replace(/['‘’`]/g, '"'); // Convert single quotes to double quotes for valid JSON - // Step 3: Find the relevant JSON block within the response - const jsonStartIndex = sanitizedResponse.indexOf('{'); - const jsonEndIndex = sanitizedResponse.lastIndexOf('}'); + const jsonStartIndex = response.indexOf('{'); + const jsonEndIndex = response.lastIndexOf('}'); // Step 4: Check if we found a valid JSON block (with both `{` and `}`) if (jsonStartIndex !== -1 && jsonEndIndex !== -1) { - const jsonResponse = sanitizedResponse.substring(jsonStartIndex, jsonEndIndex + 1); + const jsonResponse = response.substring(jsonStartIndex, jsonEndIndex + 1); // Step 5: Parse the JSON block const parsed = JSON.parse(jsonResponse); diff --git a/src/lib/components/admin/Functions.svelte b/src/lib/components/admin/Functions.svelte index 2d76a8a9c..03da04ea7 100644 --- a/src/lib/components/admin/Functions.svelte +++ b/src/lib/components/admin/Functions.svelte @@ -219,7 +219,7 @@
- {#each filteredItems as func} + {#each filteredItems as func (func.id)}
diff --git a/src/lib/components/admin/Settings/Evaluations.svelte b/src/lib/components/admin/Settings/Evaluations.svelte index bf64720bb..c0d1b4f32 100644 --- a/src/lib/components/admin/Settings/Evaluations.svelte +++ b/src/lib/components/admin/Settings/Evaluations.svelte @@ -5,6 +5,7 @@ const dispatch = createEventDispatcher(); import { getModels } from '$lib/apis'; + import { getConfig, updateConfig } from '$lib/apis/evaluations'; import Switch from '$lib/components/common/Switch.svelte'; import Spinner from '$lib/components/common/Spinner.svelte'; @@ -12,7 +13,6 @@ import Plus from '$lib/components/icons/Plus.svelte'; import Model from './Evaluations/Model.svelte'; import ArenaModelModal from './Evaluations/ArenaModelModal.svelte'; - import { getConfig, updateConfig } from '$lib/apis/evaluations'; const i18n = getContext('i18n'); @@ -27,6 +27,7 @@ if (config) { toast.success('Settings saved successfully'); + models.set(await getModels(localStorage.token)); } }; diff --git a/src/lib/components/admin/Settings/Evaluations/ArenaModelModal.svelte b/src/lib/components/admin/Settings/Evaluations/ArenaModelModal.svelte index 5f64137e4..ac45e7f2c 100644 --- a/src/lib/components/admin/Settings/Evaluations/ArenaModelModal.svelte +++ b/src/lib/components/admin/Settings/Evaluations/ArenaModelModal.svelte @@ -375,7 +375,7 @@
{#if edit}
- +
@@ -186,7 +182,7 @@
{#if models.length > 0} - {#each filteredModels as model, modelIdx (`${model.id}-${modelIdx}`)} + {#each filteredModels as model, modelIdx (model.id)}
+ import { toast } from 'svelte-sonner'; + + import { createEventDispatcher, getContext, onMount } from 'svelte'; + const i18n = getContext('i18n'); + const dispatch = createEventDispatcher(); + + import { models } from '$lib/stores'; + import { deleteAllModels } from '$lib/apis/models'; + + import Modal from '$lib/components/common/Modal.svelte'; + import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + import ModelList from './ModelList.svelte'; + import { getModelsConfig, setModelsConfig } from '$lib/apis/configs'; + import Spinner from '$lib/components/common/Spinner.svelte'; + import Minus from '$lib/components/icons/Minus.svelte'; + import Plus from '$lib/components/icons/Plus.svelte'; + + export let show = false; + export let init = () => {}; + + let config = null; + + let selectedModelId = ''; + let defaultModelIds = []; + let modelIds = []; + + let loading = false; + let showResetModal = false; + + const submitHandler = async () => { + loading = true; + + const res = await setModelsConfig(localStorage.token, { + DEFAULT_MODELS: defaultModelIds.join(','), + MODEL_ORDER_LIST: modelIds + }); + + if (res) { + toast.success($i18n.t('Models configuration saved successfully')); + init(); + show = false; + } else { + toast.error($i18n.t('Failed to save models configuration')); + } + + loading = false; + }; + + onMount(async () => { + config = await getModelsConfig(localStorage.token); + + const modelOrderList = config.MODEL_ORDER_LIST || []; + const allModelIds = $models.map((model) => model.id); + + // Create a Set for quick lookup of ordered IDs + const orderedSet = new Set(modelOrderList); + + modelIds = [ + // Add all IDs from MODEL_ORDER_LIST that exist in allModelIds + ...modelOrderList.filter((id) => orderedSet.has(id) && allModelIds.includes(id)), + // Add remaining IDs not in MODEL_ORDER_LIST, sorted alphabetically + ...allModelIds.filter((id) => !orderedSet.has(id)).sort((a, b) => a.localeCompare(b)) + ]; + }); + + + { + const res = deleteAllModels(localStorage.token); + if (res) { + toast.success($i18n.t('All models deleted successfully')); + init(); + } + }} +/> + + +
+
+
+ {$i18n.t('Configure Models')} +
+ +
+ +
+
+ {#if config} +
{ + submitHandler(); + }} + > +
+
+
+
{$i18n.t('Reorder Models')}
+
+ + +
+
+ +
+ +
+
+
+
{$i18n.t('Default Models')}
+
+ + {#if defaultModelIds.length > 0} +
+ {#each defaultModelIds as modelId, modelIdx} +
+
+ {$models.find((model) => model.id === modelId)?.name} +
+
+ +
+
+ {/each} +
+ {:else} +
+ {$i18n.t('No models selected')} +
+ {/if} + +
+ +
+ + +
+ +
+
+
+
+ +
+ + + + + +
+
+ {:else} +
+ +
+ {/if} +
+
+
+
diff --git a/src/lib/components/admin/Settings/Models/ModelList.svelte b/src/lib/components/admin/Settings/Models/ModelList.svelte new file mode 100644 index 000000000..c54d19ee4 --- /dev/null +++ b/src/lib/components/admin/Settings/Models/ModelList.svelte @@ -0,0 +1,58 @@ + + +{#if modelIds.length > 0} +
+ {#each modelIds as modelId, modelIdx (modelId)} +
+ +
+ + +
+ {#if $models.find((model) => model.id === modelId)} + {$models.find((model) => model.id === modelId).name} + {:else} + {modelId} + {/if} +
+
+
+
+ {/each} +
+{:else} +
+ {$i18n.t('No models found')} +
+{/if} diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 68f6023d7..03080d9b5 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -888,11 +888,10 @@ await tick(); // Reset chat input textarea - const chatInputContainer = document.getElementById('chat-input-container'); + const chatInputElement = document.getElementById('chat-input'); - if (chatInputContainer) { - chatInputContainer.value = ''; - chatInputContainer.style.height = ''; + if (chatInputElement) { + chatInputElement.style.height = ''; } const _files = JSON.parse(JSON.stringify(files)); @@ -1977,7 +1976,7 @@ } ); - return title; + return title ? title : (lastUserMessage?.content ?? 'New Chat'); } else { return lastUserMessage?.content ?? 'New Chat'; } @@ -2310,7 +2309,11 @@ on:submit={async (e) => { if (e.detail) { await tick(); - submitPrompt(e.detail.replaceAll('\n\n', '\n')); + submitPrompt( + ($settings?.richTextInput ?? true) + ? e.detail.replaceAll('\n\n', '\n') + : e.detail + ); } }} /> @@ -2347,7 +2350,11 @@ on:submit={async (e) => { if (e.detail) { await tick(); - submitPrompt(e.detail.replaceAll('\n\n', '\n')); + submitPrompt( + ($settings?.richTextInput ?? true) + ? e.detail.replaceAll('\n\n', '\n') + : e.detail + ); } }} /> diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index c922715d5..16e3cdb91 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -592,29 +592,6 @@ placeholder={placeholder ? placeholder : $i18n.t('Send a Message')} largeTextAsFile={$settings?.largeTextAsFile ?? false} bind:value={prompt} - on:enter={async (e) => { - const commandsContainerElement = - document.getElementById('commands-container'); - if (commandsContainerElement) { - e.preventDefault(); - - const commandOptionButton = [ - ...document.getElementsByClassName('selected-command-option-button') - ]?.at(-1); - - if (commandOptionButton) { - commandOptionButton?.click(); - return; - } - } - - if (prompt !== '') { - dispatch('submit', prompt); - } - }} - on:keypress={(e) => { - e = e.detail.event; - }} on:keydown={async (e) => { e = e.detail.event; @@ -657,34 +634,70 @@ editButton?.click(); } - if (commandsContainerElement && e.key === 'ArrowUp') { - e.preventDefault(); - commandsElement.selectUp(); + if (commandsContainerElement) { + if (commandsContainerElement && e.key === 'ArrowUp') { + e.preventDefault(); + commandsElement.selectUp(); - const commandOptionButton = [ - ...document.getElementsByClassName('selected-command-option-button') - ]?.at(-1); - commandOptionButton.scrollIntoView({ block: 'center' }); - } + const commandOptionButton = [ + ...document.getElementsByClassName('selected-command-option-button') + ]?.at(-1); + commandOptionButton.scrollIntoView({ block: 'center' }); + } - if (commandsContainerElement && e.key === 'ArrowDown') { - e.preventDefault(); - commandsElement.selectDown(); + if (commandsContainerElement && e.key === 'ArrowDown') { + e.preventDefault(); + commandsElement.selectDown(); - const commandOptionButton = [ - ...document.getElementsByClassName('selected-command-option-button') - ]?.at(-1); - commandOptionButton.scrollIntoView({ block: 'center' }); - } + const commandOptionButton = [ + ...document.getElementsByClassName('selected-command-option-button') + ]?.at(-1); + commandOptionButton.scrollIntoView({ block: 'center' }); + } - if (commandsContainerElement && e.key === 'Tab') { - e.preventDefault(); + if (commandsContainerElement && e.key === 'Tab') { + e.preventDefault(); - const commandOptionButton = [ - ...document.getElementsByClassName('selected-command-option-button') - ]?.at(-1); + const commandOptionButton = [ + ...document.getElementsByClassName('selected-command-option-button') + ]?.at(-1); - commandOptionButton?.click(); + commandOptionButton?.click(); + } + + if (commandsContainerElement && e.key === 'Enter') { + e.preventDefault(); + + const commandOptionButton = [ + ...document.getElementsByClassName('selected-command-option-button') + ]?.at(-1); + + if (commandOptionButton) { + commandOptionButton?.click(); + } else { + document.getElementById('send-message-button')?.click(); + } + } + } else { + if ( + !$mobile || + !( + 'ontouchstart' in window || + navigator.maxTouchPoints > 0 || + navigator.msMaxTouchPoints > 0 + ) + ) { + // Prevent Enter key from creating a new line + // Uses keyCode '13' for Enter key for chinese/japanese keyboards + if (e.keyCode === 13 && !e.shiftKey) { + e.preventDefault(); + } + + // Submit the prompt when Enter key is pressed + if (prompt !== '' && e.keyCode === 13 && !e.shiftKey) { + dispatch('submit', prompt); + } + } } if (e.key === 'Escape') { @@ -881,7 +894,6 @@ on:input={async (e) => { e.target.style.height = ''; e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px'; - user = null; }} on:focus={async (e) => { e.target.style.height = ''; diff --git a/src/lib/components/chat/Messages/CitationsModal.svelte b/src/lib/components/chat/Messages/CitationsModal.svelte index 88d6c7f6e..9c6b23ecd 100644 --- a/src/lib/components/chat/Messages/CitationsModal.svelte +++ b/src/lib/components/chat/Messages/CitationsModal.svelte @@ -38,7 +38,9 @@ }; }); if (mergedDocuments.every((doc) => doc.distance !== undefined)) { - mergedDocuments.sort((a, b) => (a.distance ?? Infinity) - (b.distance ?? Infinity)); + mergedDocuments = mergedDocuments.sort( + (a, b) => (b.distance ?? Infinity) - (a.distance ?? Infinity) + ); } } diff --git a/src/lib/components/chat/ModelSelector.svelte b/src/lib/components/chat/ModelSelector.svelte index 9b16a6500..9b77cd8ce 100644 --- a/src/lib/components/chat/ModelSelector.svelte +++ b/src/lib/components/chat/ModelSelector.svelte @@ -5,9 +5,7 @@ import Selector from './ModelSelector/Selector.svelte'; import Tooltip from '../common/Tooltip.svelte'; - import { setDefaultModels } from '$lib/apis/configs'; import { updateUserSettings } from '$lib/apis/users'; - const i18n = getContext('i18n'); export let selectedModels = ['']; diff --git a/src/lib/components/chat/Settings/General.svelte b/src/lib/components/chat/Settings/General.svelte index 9c1ccb7f7..694648206 100644 --- a/src/lib/components/chat/Settings/General.svelte +++ b/src/lib/components/chat/Settings/General.svelte @@ -55,6 +55,7 @@ mirostat_tau: null, top_k: null, top_p: null, + min_p: null, stop: null, tfs_z: null, num_ctx: null, @@ -340,6 +341,7 @@ mirostat_tau: params.mirostat_tau !== null ? params.mirostat_tau : undefined, top_k: params.top_k !== null ? params.top_k : undefined, top_p: params.top_p !== null ? params.top_p : undefined, + min_p: params.min_p !== null ? params.min_p : undefined, tfs_z: params.tfs_z !== null ? params.tfs_z : undefined, num_ctx: params.num_ctx !== null ? params.num_ctx : undefined, num_batch: params.num_batch !== null ? params.num_batch : undefined, diff --git a/src/lib/components/common/Modal.svelte b/src/lib/components/common/Modal.svelte index 795d3d0f1..86c741e23 100644 --- a/src/lib/components/common/Modal.svelte +++ b/src/lib/components/common/Modal.svelte @@ -6,7 +6,7 @@ export let show = true; export let size = 'md'; - export let className = 'bg-gray-50 dark:bg-gray-900 rounded-2xl'; + export let className = 'bg-gray-50 dark:bg-gray-900 rounded-2xl'; let modalElement = null; let mounted = false; @@ -65,7 +65,7 @@