diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e67c9fbd5..0316bb1035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,66 @@ 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.6.29] - 2025-09-17 + +### Added + +- 🎨 The chat input menu has been completely overhauled with a revolutionary new design, consolidating attachments under a unified '+' button, organizing integrations into a streamlined options menu, and introducing powerful, interactive selectors for attaching chats, notes, and knowledge base items. [Commit](https://github.com/open-webui/open-webui/commit/a68342d5a887e36695e21f8c2aec593b159654ff), [Commit](https://github.com/open-webui/open-webui/commit/96b8aaf83ff341fef432649366bc5155bac6cf20), [Commit](https://github.com/open-webui/open-webui/commit/4977e6d50f7b931372c96dd5979ca635d58aeb78), [Commit](https://github.com/open-webui/open-webui/commit/d973db829f7ec98b8f8fe7d3b2822d588e79f94e), [Commit](https://github.com/open-webui/open-webui/commit/d4c628de09654df76653ad9bce9cb3263e2f27c8), [Commit](https://github.com/open-webui/open-webui/commit/cd740f436db4ea308dbede14ef7ff56e8126f51b), [Commit](https://github.com/open-webui/open-webui/commit/5c2db102d06b5c18beb248d795682ff422e9b6d1), [Commit](https://github.com/open-webui/open-webui/commit/031cf38655a1a2973194d2eaa0fbbd17aca8ee92), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/3ed0a6d11fea1a054e0bc8aa8dfbe417c7c53e51), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/eadec9e86e01bc8f9fb90dfe7a7ae4fc3bfa6420), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/c03ca7270e64e3a002d321237160c0ddaf2bb129), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/b53ddfbd19aa94e9cbf7210acb31c3cfafafa5fe), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/c923461882fcde30ae297a95e91176c95b9b72e1) +- 🤖 AI models can now be mentioned in channels to automatically generate responses, enabling multi-model conversations where mentioned models participate directly in threaded discussions with full context awareness. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/4fe97d8794ee18e087790caab9e5d82886006145) +- 💬 The Channels feature now utilizes the modern rich text editor, including support for '/', '@', and '#' command suggestions. [Commit](https://github.com/open-webui/open-webui/commit/06c1426e14ac0dfaf723485dbbc9723a4d89aba9), [Commit](https://github.com/open-webui/open-webui/commit/02f7c3258b62970ce79716f75d15467a96565054) +- 📎 Channel message input now supports direct paste functionality for images and files from the clipboard, streamlining content sharing workflows. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/6549fc839f86c40c26c2ef4dedcaf763a9304418) +- ⚙️ Models can now be configured with default features (Web Search, Image Generation) and filters that automatically activate when a user selects the model. [Commit](https://github.com/open-webui/open-webui/commit/9a555478273355a5177bfc7f7211c64778e4c8de), [Commit](https://github.com/open-webui/open-webui/commit/384a53b339820068e92f7eaea0d9f3e0536c19c2), [Commit](https://github.com/open-webui/open-webui/commit/d7f43bfc1a30c065def8c50d77c2579c1a3c5c67), [Commit](https://github.com/open-webui/open-webui/commit/6a67a2217cc5946ad771e479e3a37ac213210748) +- 💬 The ability to reference other chats as context within a conversation was added via the attachment menu. [Commit](https://github.com/open-webui/open-webui/commit/e097bbdf11ae4975c622e086df00d054291cdeb3), [Commit](https://github.com/open-webui/open-webui/commit/f3cd2ffb18e7dedbe88430f9ae7caa6b3cfd79d0), [Commit](https://github.com/open-webui/open-webui/commit/74263c872c5d574a9bb0944d7984f748dc772dba), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/aa8ab349ed2fcb46d1cf994b9c0de2ec2ea35d0d), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/025eef754f0d46789981defd473d001e3b1d0ca2) +- 🎨 The command suggestion UI for prompts ('/'), models ('@'), and knowledge ('#') was completely overhauled with a more responsive and keyboard-navigable interface. [Commit](https://github.com/open-webui/open-webui/commit/6b69c4da0fb9329ccf7024483960e070cf52ccab), [Commit](https://github.com/open-webui/open-webui/commit/06a6855f844456eceaa4d410c93379460e208202), [Commit](https://github.com/open-webui/open-webui/commit/c55f5578280b936cf581a743df3703e3db1afd54), [Commit](https://github.com/open-webui/open-webui/commit/f68d1ba394d4423d369f827894cde99d760b2402) +- 👥 User and channel suggestions were added to the mention system, enabling '@' mentions for users and models, and '#' mentions for channels with searchable user lookup and clickable navigation. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/bbd1d2b58c89b35daea234f1fc9208f2af840899), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/aef1e06f0bb72065a25579c982dd49157e320268), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/779db74d7e9b7b00d099b7d65cfbc8a831e74690) +- 📁 Folder functionality was enhanced with custom background image support, improved drag-and-drop capabilities for moving folders to root level, and better menu interactions. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/2a234829f5dfdfde27fdfd30591caa908340efb4), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/2b1ee8b0dc5f7c0caaafdd218f20705059fa72e2), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/b1e5bc8e490745f701909c19b6a444b67c04660e), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/3e584132686372dfeef187596a7c557aa5f48308) +- ☁️ OneDrive integration configuration now supports selecting between personal and work/school account types via ENABLE_ONEDRIVE_PERSONAL and ENABLE_ONEDRIVE_BUSINESS environment variables. [#17354](https://github.com/open-webui/open-webui/pull/17354), [Commit](https://github.com/open-webui/open-webui/commit/e1e3009a30f9808ce06582d81a60e391f5ca09ec), [Docs:#697](https://github.com/open-webui/docs/pull/697) +- ⚡ Mermaid.js is now dynamically loaded on demand, significantly reducing first-screen loading time and improving initial page performance. [#17476](https://github.com/open-webui/open-webui/issues/17476), [#17477](https://github.com/open-webui/open-webui/pull/17477) +- ⚡ Azure MSAL browser library is now dynamically loaded on demand, reducing initial bundle size by 730KB and improving first-screen loading speed. [#17479](https://github.com/open-webui/open-webui/pull/17479) +- ⚡ CodeEditor component is now dynamically loaded on demand, reducing initial bundle size by 1MB and improving first-screen loading speed. [#17498](https://github.com/open-webui/open-webui/pull/17498) +- ⚡ Hugging Face Transformers library is now dynamically loaded on demand, reducing initial bundle size by 1.9MB and improving first-screen loading speed. [#17499](https://github.com/open-webui/open-webui/pull/17499) +- ⚡ jsPDF and html2canvas-pro libraries are now dynamically loaded on demand, reducing initial bundle size by 980KB and improving first-screen loading speed. [#17502](https://github.com/open-webui/open-webui/pull/17502) +- ⚡ Leaflet mapping library is now dynamically loaded on demand, reducing initial bundle size by 454KB and improving first-screen loading speed. [#17503](https://github.com/open-webui/open-webui/pull/17503) +- 📊 OpenTelemetry metrics collection was enhanced to properly handle HTTP 500 errors and ensure metrics are recorded even during exceptions. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/b14617a653c6bdcfd3102c12f971924fd1faf572) +- 🔒 OAuth token retrieval logic was refactored, improving the reliability and consistency of authentication handling across the backend. [Commit](https://github.com/open-webui/open-webui/commit/6c0a5fa91cdbf6ffb74667ee61ca96bebfdfbc50) +- 💻 Code block output processing was improved to handle Python execution results more reliably, along with refined visual styling and button layouts. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/0e5320c39e308ff97f2ca9e289618af12479eb6e) +- ⚡ Message input processing was optimized to skip unnecessary text variable handling when input is empty, improving performance. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/e1386fe80b77126a12dabc4ad058abe9b024b275) +- 📄 Individual chat PDF export was added to the sidebar chat menu, allowing users to export single conversations as PDF documents with both stylized and plain text options. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/d041d58bb619689cd04a391b4f8191b23941ca62) +- 🛠️ Function validation was enhanced with improved valve validation and better error handling during function loading and synchronization. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/e66e0526ed6a116323285f79f44237538b6c75e6), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/8edfd29102e0a61777b23d3575eaa30be37b59a5) +- 🔔 Notification toast interaction was enhanced with drag detection to prevent accidental clicks and added keyboard support for accessibility. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/621e7679c427b6f0efa85f95235319238bf171ad) +- 🗓️ Improved date and time formatting dynamically adapts to the selected language, ensuring consistent localization across the UI. [#17409](https://github.com/open-webui/open-webui/pull/17409), [Commit](https://github.com/open-webui/open-webui/commit/2227f24bd6d861b1fad8d2cabacf7d62ce137d0c) +- 🔒 Feishu SSO integration was added, allowing users to authenticate via Feishu. [#17284](https://github.com/open-webui/open-webui/pull/17284), [Docs:#685](https://github.com/open-webui/docs/pull/685) +- 🔠 Toggle filters in the chat input options menu are now sorted alphabetically for easier navigation. [Commit](https://github.com/open-webui/open-webui/commit/ca853ca4656180487afcd84230d214f91db52533) +- 🎨 Long chat titles in the sidebar are now truncated to prevent text overflow and maintain a clean layout. [#17356](https://github.com/open-webui/open-webui/pull/17356) +- 🎨 Temporary chat interface design was refined with improved layout and visual consistency. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/67549dcadd670285d491bd41daf3d081a70fd094), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/2ca34217e68f3b439899c75881dfb050f49c9eb2), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/fb02ec52a5df3f58b53db4ab3a995c15f83503cd) +- 🎨 Download icon consistency was improved across the entire interface by standardizing the icon component used in menus, functions, tools, and export features. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/596be451ece7e11b5cd25465d49670c27a1cb33f) +- 🎨 Settings interface was enhanced with improved iconography and reorganized the 'Chats' section into 'Data Controls' for better clarity. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/8bf0b40fdd978b5af6548a6e1fb3aabd90bcd5cd) +- 🔄 Various improvements were implemented across the frontend and backend to enhance performance, stability, and security. +- 🌐 Translations for Finnish, German, Kabyle, Portuguese (Brazil), Simplified Chinese, Spanish (Spain), and Traditional Chinese (Taiwan) were enhanced and expanded. + +### Fixed + +- 📚 Knowledge base permission logic was corrected to ensure private collection owners can access their own content when embedding bypass is enabled. [#17432](https://github.com/open-webui/open-webui/issues/17432), [Commit](https://github.com/open-webui/open-webui/commit/a51f0c30ec1472d71487eab3e15d0351a2716b12) +- ⚙️ Connection URL editing in Admin Settings now properly saves changes instead of reverting to original values, fixing issues with both Ollama and OpenAI-compatible endpoints. [#17435](https://github.com/open-webui/open-webui/issues/17435), [Commit](https://github.com/open-webui/open-webui/commit/e4c864de7eb0d577843a80688677ce3659d1f81f) +- 📊 Usage information collection from Google models was corrected to handle providers that send usage data alongside content chunks instead of separately. [#17421](https://github.com/open-webui/open-webui/pull/17421), [Commit](https://github.com/open-webui/open-webui/commit/c2f98a4cd29ed738f395fef09c42ab8e73cd46a0) +- ⚙️ Settings modal scrolling issue was resolved by moving image compression controls to a dedicated modal, preventing the main settings from becoming scrollable out of view. [#17474](https://github.com/open-webui/open-webui/issues/17474), [Commit](https://github.com/open-webui/open-webui/commit/fed5615c19b0045a55b0be426b468a57bfda4b66) +- 📁 Folder click behavior was improved to prevent accidental actions by implementing proper double-click detection and timing delays for folder expansion and selection. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/19e3214997170eea6ee92452e8c778e04a28e396) +- 🔐 Access control component reliability was improved with better null checking and error handling for group permissions and private access scenarios. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/c8780a7f934c5e49a21b438f2f30232f83cf75d2), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/32015c392dbc6b7367a6a91d9e173e675ea3402c) +- 🔗 The citation modal now correctly displays and links to external web page sources in addition to internal documents. [Commit](https://github.com/open-webui/open-webui/commit/9208a84185a7e59524f00a7576667d493c3ac7d4) +- 🔗 Web and YouTube attachment handling was fixed, ensuring their content is now reliably processed and included in the chat context for retrieval. [Commit](https://github.com/open-webui/open-webui/commit/210197fd438b52080cda5d6ce3d47b92cdc264c8) +- 📂 Large file upload failures are resolved by correcting the processing logic for scenarios where document embedding is bypassed. [Commit](https://github.com/open-webui/open-webui/commit/051b6daa8299fd332503bd584563556e2ae6adab) +- 🌐 Rich text input placeholder text now correctly updates when the interface language is switched, ensuring proper localization. [#17473](https://github.com/open-webui/open-webui/pull/17473), [Commit](https://github.com/open-webui/open-webui/commit/77358031f5077e6efe5cc08d8d4e5831c7cd1cd9) +- 📊 Llama.cpp server timing metrics are now correctly parsed and displayed by fixing a typo in the response handling. [#17350](https://github.com/open-webui/open-webui/issues/17350), [Commit](https://github.com/open-webui/open-webui/commit/cf72f5503f39834b9da44ebbb426a3674dad0caa) +- 🛠️ Filter functions with file_handler configuration now properly handle messages without file attachments, preventing runtime errors. [#17423](https://github.com/open-webui/open-webui/pull/17423) +- 🔔 Channel notification delivery was fixed to properly handle background task execution and user access checking. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/1077b2ac8b96e49c2ad2620e76eb65bbb2a3a1f3) + +### Changed + +- 📝 Prompt template variables are now optional by default instead of being forced as required, allowing flexible workflows with optional metadata fields. [#17447](https://github.com/open-webui/open-webui/issues/17447), [Commit](https://github.com/open-webui/open-webui/commit/d5824b1b495fcf86e57171769bcec2a0f698b070), [Docs:#696](https://github.com/open-webui/docs/pull/696) +- 🛠️ Direct external tool servers now require explicit user selection from the input interface instead of being automatically included in conversations, providing better control over tool usage. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/0f04227c34ca32746c43a9323e2df32299fcb6af), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/99bba12de279dd55c55ded35b2e4f819af1c9ab5) +- 📺 Widescreen mode option was removed from Channels interface, with all channel layouts now using full-width display. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/d46b7b8f1b99a8054b55031fe935c8a16d5ec956) +- 🎛️ The plain textarea input option was deprecated, and the custom text editor is now the standard for all chat inputs. [Commit](https://github.com/open-webui/open-webui/commit/153afd832ccd12a1e5fd99b085008d080872c161) + ## [0.6.28] - 2025-09-10 ### Added diff --git a/backend/dev.sh b/backend/dev.sh index 504b8f7554..042fbd9efa 100755 --- a/backend/dev.sh +++ b/backend/dev.sh @@ -1,3 +1,3 @@ -export CORS_ALLOW_ORIGIN="http://localhost:5173" +export CORS_ALLOW_ORIGIN="http://localhost:5173;http://localhost:8080" PORT="${PORT:-8080}" uvicorn open_webui.main:app --port $PORT --host 0.0.0.0 --forwarded-allow-ips '*' --reload diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 11698d87af..ca090efa22 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -513,6 +513,30 @@ OAUTH_GROUPS_CLAIM = PersistentConfig( os.environ.get("OAUTH_GROUPS_CLAIM", os.environ.get("OAUTH_GROUP_CLAIM", "groups")), ) +FEISHU_CLIENT_ID = PersistentConfig( + "FEISHU_CLIENT_ID", + "oauth.feishu.client_id", + os.environ.get("FEISHU_CLIENT_ID", ""), +) + +FEISHU_CLIENT_SECRET = PersistentConfig( + "FEISHU_CLIENT_SECRET", + "oauth.feishu.client_secret", + os.environ.get("FEISHU_CLIENT_SECRET", ""), +) + +FEISHU_OAUTH_SCOPE = PersistentConfig( + "FEISHU_OAUTH_SCOPE", + "oauth.feishu.scope", + os.environ.get("FEISHU_OAUTH_SCOPE", "contact:user.base:readonly"), +) + +FEISHU_REDIRECT_URI = PersistentConfig( + "FEISHU_REDIRECT_URI", + "oauth.feishu.redirect_uri", + os.environ.get("FEISHU_REDIRECT_URI", ""), +) + ENABLE_OAUTH_ROLE_MANAGEMENT = PersistentConfig( "ENABLE_OAUTH_ROLE_MANAGEMENT", "oauth.enable_role_mapping", @@ -705,6 +729,33 @@ def load_oauth_providers(): "register": oidc_oauth_register, } + if FEISHU_CLIENT_ID.value and FEISHU_CLIENT_SECRET.value: + + def feishu_oauth_register(client: OAuth): + client.register( + name="feishu", + client_id=FEISHU_CLIENT_ID.value, + client_secret=FEISHU_CLIENT_SECRET.value, + access_token_url="https://open.feishu.cn/open-apis/authen/v2/oauth/token", + authorize_url="https://accounts.feishu.cn/open-apis/authen/v1/authorize", + api_base_url="https://open.feishu.cn/open-apis", + userinfo_endpoint="https://open.feishu.cn/open-apis/authen/v1/user_info", + client_kwargs={ + "scope": FEISHU_OAUTH_SCOPE.value, + **( + {"timeout": int(OAUTH_TIMEOUT.value)} + if OAUTH_TIMEOUT.value + else {} + ), + }, + redirect_uri=FEISHU_REDIRECT_URI.value, + ) + + OAUTH_PROVIDERS["feishu"] = { + "register": feishu_oauth_register, + "sub_claim": "user_id", + } + configured_providers = [] if GOOGLE_CLIENT_ID.value: configured_providers.append("Google") @@ -712,6 +763,8 @@ def load_oauth_providers(): configured_providers.append("Microsoft") if GITHUB_CLIENT_ID.value: configured_providers.append("GitHub") + if FEISHU_CLIENT_ID.value: + configured_providers.append("Feishu") if configured_providers and not OPENID_PROVIDER_URL.value: provider_list = ", ".join(configured_providers) @@ -2115,6 +2168,12 @@ ENABLE_ONEDRIVE_INTEGRATION = PersistentConfig( "onedrive.enable", os.getenv("ENABLE_ONEDRIVE_INTEGRATION", "False").lower() == "true", ) +ENABLE_ONEDRIVE_PERSONAL = ( + os.environ.get("ENABLE_ONEDRIVE_PERSONAL", "True").lower() == "true" +) +ENABLE_ONEDRIVE_BUSINESS = ( + os.environ.get("ENABLE_ONEDRIVE_BUSINESS", "True").lower() == "true" +) ONEDRIVE_CLIENT_ID = PersistentConfig( "ONEDRIVE_CLIENT_ID", diff --git a/backend/open_webui/functions.py b/backend/open_webui/functions.py index 4122cbbe0d..af6dd1ce1a 100644 --- a/backend/open_webui/functions.py +++ b/backend/open_webui/functions.py @@ -19,6 +19,7 @@ from fastapi import ( from starlette.responses import Response, StreamingResponse +from open_webui.constants import ERROR_MESSAGES from open_webui.socket.main import ( get_event_call, get_event_emitter, @@ -60,8 +61,20 @@ def get_function_module_by_id(request: Request, pipe_id: str): function_module, _, _ = get_function_module_from_cache(request, pipe_id) if hasattr(function_module, "valves") and hasattr(function_module, "Valves"): + Valves = function_module.Valves valves = Functions.get_function_valves_by_id(pipe_id) - function_module.valves = function_module.Valves(**(valves if valves else {})) + + if valves: + try: + function_module.valves = Valves( + **{k: v for k, v in valves.items() if v is not None} + ) + except Exception as e: + log.exception(f"Error loading valves for function {pipe_id}: {e}") + raise e + else: + function_module.valves = Valves() + return function_module @@ -70,65 +83,69 @@ async def get_function_models(request): pipe_models = [] for pipe in pipes: - function_module = get_function_module_by_id(request, pipe.id) + try: + function_module = get_function_module_by_id(request, pipe.id) - # Check if function is a manifold - if hasattr(function_module, "pipes"): - sub_pipes = [] - - # Handle pipes being a list, sync function, or async function - try: - if callable(function_module.pipes): - if asyncio.iscoroutinefunction(function_module.pipes): - sub_pipes = await function_module.pipes() - else: - sub_pipes = function_module.pipes() - else: - sub_pipes = function_module.pipes - except Exception as e: - log.exception(e) + # Check if function is a manifold + if hasattr(function_module, "pipes"): sub_pipes = [] - log.debug( - f"get_function_models: function '{pipe.id}' is a manifold of {sub_pipes}" - ) + # Handle pipes being a list, sync function, or async function + try: + if callable(function_module.pipes): + if asyncio.iscoroutinefunction(function_module.pipes): + sub_pipes = await function_module.pipes() + else: + sub_pipes = function_module.pipes() + else: + sub_pipes = function_module.pipes + except Exception as e: + log.exception(e) + sub_pipes = [] - for p in sub_pipes: - sub_pipe_id = f'{pipe.id}.{p["id"]}' - sub_pipe_name = p["name"] + log.debug( + f"get_function_models: function '{pipe.id}' is a manifold of {sub_pipes}" + ) - if hasattr(function_module, "name"): - sub_pipe_name = f"{function_module.name}{sub_pipe_name}" + for p in sub_pipes: + sub_pipe_id = f'{pipe.id}.{p["id"]}' + sub_pipe_name = p["name"] - pipe_flag = {"type": pipe.type} + if hasattr(function_module, "name"): + sub_pipe_name = f"{function_module.name}{sub_pipe_name}" + + pipe_flag = {"type": pipe.type} + + pipe_models.append( + { + "id": sub_pipe_id, + "name": sub_pipe_name, + "object": "model", + "created": pipe.created_at, + "owned_by": "openai", + "pipe": pipe_flag, + } + ) + else: + pipe_flag = {"type": "pipe"} + + log.debug( + f"get_function_models: function '{pipe.id}' is a single pipe {{ 'id': {pipe.id}, 'name': {pipe.name} }}" + ) pipe_models.append( { - "id": sub_pipe_id, - "name": sub_pipe_name, + "id": pipe.id, + "name": pipe.name, "object": "model", "created": pipe.created_at, "owned_by": "openai", "pipe": pipe_flag, } ) - else: - pipe_flag = {"type": "pipe"} - - log.debug( - f"get_function_models: function '{pipe.id}' is a single pipe {{ 'id': {pipe.id}, 'name': {pipe.name} }}" - ) - - pipe_models.append( - { - "id": pipe.id, - "name": pipe.name, - "object": "model", - "created": pipe.created_at, - "owned_by": "openai", - "pipe": pipe_flag, - } - ) + except Exception as e: + log.exception(e) + continue return pipe_models @@ -221,10 +238,11 @@ async def generate_function_chat_completion( oauth_token = None try: - oauth_token = request.app.state.oauth_manager.get_oauth_token( - user.id, - request.cookies.get("oauth_session_id", None), - ) + if request.cookies.get("oauth_session_id", None): + oauth_token = request.app.state.oauth_manager.get_oauth_token( + user.id, + request.cookies.get("oauth_session_id", None), + ) except Exception as e: log.error(f"Error getting OAuth token: {e}") diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index a5d55f75ab..bf25c91cd0 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -448,6 +448,7 @@ from open_webui.utils.models import ( get_all_models, get_all_base_models, check_model_access, + get_filtered_models, ) from open_webui.utils.chat import ( generate_chat_completion as chat_completion_handler, @@ -1291,33 +1292,6 @@ if audit_level != AuditLevel.NONE: async def get_models( request: Request, refresh: bool = False, user=Depends(get_verified_user) ): - def get_filtered_models(models, user): - filtered_models = [] - for model in models: - if model.get("arena"): - if has_access( - user.id, - type="read", - access_control=model.get("info", {}) - .get("meta", {}) - .get("access_control", {}), - ): - filtered_models.append(model) - continue - - model_info = Models.get_model_by_id(model["id"]) - if model_info: - if ( - (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) - or user.id == model_info.user_id - or has_access( - user.id, type="read", access_control=model_info.access_control - ) - ): - filtered_models.append(model) - - return filtered_models - all_models = await get_all_models(request, refresh=refresh, user=user) models = [] @@ -1353,12 +1327,7 @@ async def get_models( ) ) - # Filter out models that the user does not have access to - if ( - user.role == "user" - or (user.role == "admin" and not BYPASS_ADMIN_ACCESS_CONTROL) - ) and not BYPASS_MODEL_ACCESS_CONTROL: - models = get_filtered_models(models, user) + models = get_filtered_models(models, user) log.debug( f"/api/models returned filtered models accessible to the user: {json.dumps([model.get('id') for model in models])}" @@ -1418,14 +1387,6 @@ async def chat_completion( model_item = form_data.pop("model_item", {}) tasks = form_data.pop("background_tasks", None) - oauth_token = None - try: - oauth_token = request.app.state.oauth_manager.get_oauth_token( - user.id, request.cookies.get("oauth_session_id", None) - ) - except Exception as e: - log.error(f"Error getting OAuth token: {e}") - metadata = {} try: if not model_item.get("direct", False): @@ -1738,6 +1699,14 @@ async def get_app_config(request: Request): "enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS, "enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, "enable_onedrive_integration": app.state.config.ENABLE_ONEDRIVE_INTEGRATION, + **( + { + "enable_onedrive_personal": app.state.config.ENABLE_ONEDRIVE_PERSONAL, + "enable_onedrive_business": app.state.config.ENABLE_ONEDRIVE_BUSINESS, + } + if app.state.config.ENABLE_ONEDRIVE_INTEGRATION + else {} + ), } if user is not None else {} diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 56f992806a..cadb5a3a79 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -236,7 +236,7 @@ class ChatTable: return chat.chat.get("title", "New Chat") - def get_messages_by_chat_id(self, id: str) -> Optional[dict]: + def get_messages_map_by_chat_id(self, id: str) -> Optional[dict]: chat = self.get_chat_by_id(id) if chat is None: return None diff --git a/backend/open_webui/models/functions.py b/backend/open_webui/models/functions.py index 2bb6d60889..e8ce3aa811 100644 --- a/backend/open_webui/models/functions.py +++ b/backend/open_webui/models/functions.py @@ -37,6 +37,7 @@ class Function(Base): class FunctionMeta(BaseModel): description: Optional[str] = None manifest: Optional[dict] = {} + model_config = ConfigDict(extra="allow") class FunctionModel(BaseModel): @@ -260,6 +261,29 @@ class FunctionsTable: except Exception: return None + def update_function_metadata_by_id( + self, id: str, metadata: dict + ) -> Optional[FunctionModel]: + with get_db() as db: + try: + function = db.get(Function, id) + + if function: + if function.meta: + function.meta = {**function.meta, **metadata} + else: + function.meta = metadata + + function.updated_at = int(time.time()) + db.commit() + db.refresh(function) + return self.get_function_by_id(id) + else: + return None + except Exception as e: + log.exception(f"Error updating function metadata by id {id}: {e}") + return None + def get_user_valves_by_id_and_user_id( self, id: str, user_id: str ) -> Optional[dict]: diff --git a/backend/open_webui/models/messages.py b/backend/open_webui/models/messages.py index a27ae52519..ff4553ee9d 100644 --- a/backend/open_webui/models/messages.py +++ b/backend/open_webui/models/messages.py @@ -201,8 +201,14 @@ class MessageTable: with get_db() as db: message = db.get(Message, id) message.content = form_data.content - message.data = form_data.data - message.meta = form_data.meta + message.data = { + **(message.data if message.data else {}), + **(form_data.data if form_data.data else {}), + } + message.meta = { + **(message.meta if message.meta else {}), + **(form_data.meta if form_data.meta else {}), + } message.updated_at = int(time.time_ns()) db.commit() db.refresh(message) diff --git a/backend/open_webui/models/notes.py b/backend/open_webui/models/notes.py index c720ff80a4..b61e820eae 100644 --- a/backend/open_webui/models/notes.py +++ b/backend/open_webui/models/notes.py @@ -97,15 +97,26 @@ class NoteTable: db.commit() return note - def get_notes(self) -> list[NoteModel]: + def get_notes( + self, skip: Optional[int] = None, limit: Optional[int] = None + ) -> list[NoteModel]: with get_db() as db: - notes = db.query(Note).order_by(Note.updated_at.desc()).all() + query = db.query(Note).order_by(Note.updated_at.desc()) + if skip is not None: + query = query.offset(skip) + if limit is not None: + query = query.limit(limit) + notes = query.all() return [NoteModel.model_validate(note) for note in notes] def get_notes_by_user_id( - self, user_id: str, permission: str = "write" + self, + user_id: str, + permission: str = "write", + skip: Optional[int] = None, + limit: Optional[int] = None, ) -> list[NoteModel]: - notes = self.get_notes() + notes = self.get_notes(skip=skip, limit=limit) user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)} return [ note diff --git a/backend/open_webui/models/users.py b/backend/open_webui/models/users.py index 620a746eed..05000744dd 100644 --- a/backend/open_webui/models/users.py +++ b/backend/open_webui/models/users.py @@ -107,11 +107,21 @@ class UserInfoResponse(BaseModel): role: str +class UserIdNameResponse(BaseModel): + id: str + name: str + + class UserInfoListResponse(BaseModel): users: list[UserInfoResponse] total: int +class UserIdNameListResponse(BaseModel): + users: list[UserIdNameResponse] + total: int + + class UserResponse(BaseModel): id: str name: str @@ -210,7 +220,7 @@ class UsersTable: filter: Optional[dict] = None, skip: Optional[int] = None, limit: Optional[int] = None, - ) -> UserListResponse: + ) -> dict: with get_db() as db: query = db.query(User) diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index dead8458cb..aec8de6846 100644 --- a/backend/open_webui/retrieval/utils.py +++ b/backend/open_webui/retrieval/utils.py @@ -19,10 +19,13 @@ from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT from open_webui.models.users import UserModel from open_webui.models.files import Files from open_webui.models.knowledge import Knowledges + +from open_webui.models.chats import Chats from open_webui.models.notes import Notes from open_webui.retrieval.vector.main import GetResult from open_webui.utils.access_control import has_access +from open_webui.utils.misc import get_message_list from open_webui.env import ( @@ -432,13 +435,14 @@ def get_embedding_function( if isinstance(query, list): embeddings = [] for i in range(0, len(query), embedding_batch_size): - embeddings.extend( - func( - query[i : i + embedding_batch_size], - prefix=prefix, - user=user, - ) + batch_embeddings = func( + query[i : i + embedding_batch_size], + prefix=prefix, + user=user, ) + + if isinstance(batch_embeddings, list): + embeddings.extend(batch_embeddings) return embeddings else: return func(query, prefix, user) @@ -490,25 +494,37 @@ def get_sources_from_items( # Raw Text # Used during temporary chat file uploads or web page & youtube attachements - if item.get("collection_name"): - # If item has a collection name, use it - collection_names.append(item.get("collection_name")) - elif item.get("file"): - # if item has file data, use it - query_result = { - "documents": [ - [item.get("file", {}).get("data", {}).get("content")] - ], - "metadatas": [[item.get("file", {}).get("meta", {})]], - } - else: - # Fallback to item content - query_result = { - "documents": [[item.get("content")]], - "metadatas": [ - [{"file_id": item.get("id"), "name": item.get("name")}] - ], - } + if item.get("context") == "full": + if item.get("file"): + # if item has file data, use it + query_result = { + "documents": [ + [item.get("file", {}).get("data", {}).get("content")] + ], + "metadatas": [[item.get("file", {}).get("meta", {})]], + } + + if query_result is None: + # Fallback + if item.get("collection_name"): + # If item has a collection name, use it + collection_names.append(item.get("collection_name")) + elif item.get("file"): + # If item has file data, use it + query_result = { + "documents": [ + [item.get("file", {}).get("data", {}).get("content")] + ], + "metadatas": [[item.get("file", {}).get("meta", {})]], + } + else: + # Fallback to item content + query_result = { + "documents": [[item.get("content")]], + "metadatas": [ + [{"file_id": item.get("id"), "name": item.get("name")}] + ], + } elif item.get("type") == "note": # Note Attached @@ -525,6 +541,30 @@ def get_sources_from_items( "metadatas": [[{"file_id": note.id, "name": note.title}]], } + elif item.get("type") == "chat": + # Chat Attached + chat = Chats.get_chat_by_id(item.get("id")) + + if chat and (user.role == "admin" or chat.user_id == user.id): + messages_map = chat.chat.get("history", {}).get("messages", {}) + message_id = chat.chat.get("history", {}).get("currentId") + + if messages_map and message_id: + # Reconstruct the message list in order + message_list = get_message_list(messages_map, message_id) + message_history = "\n".join( + [ + f"#### {m.get('role', 'user').capitalize()}\n{m.get('content')}\n" + for m in message_list + ] + ) + + # User has access to the chat + query_result = { + "documents": [[message_history]], + "metadatas": [[{"file_id": chat.id, "name": chat.title}]], + } + elif item.get("type") == "file": if ( item.get("context") == "full" @@ -581,6 +621,7 @@ def get_sources_from_items( if knowledge_base and ( user.role == "admin" + or knowledge_base.user_id == user.id or has_access(user.id, "read", knowledge_base.access_control) ): diff --git a/backend/open_webui/routers/audio.py b/backend/open_webui/routers/audio.py index c4a187b50d..100610a83a 100644 --- a/backend/open_webui/routers/audio.py +++ b/backend/open_webui/routers/audio.py @@ -550,7 +550,7 @@ def transcription_handler(request, file_path, metadata): metadata = metadata or {} languages = [ - metadata.get("language", None) if WHISPER_LANGUAGE == "" else WHISPER_LANGUAGE, + metadata.get("language", None) if not WHISPER_LANGUAGE else WHISPER_LANGUAGE, None, # Always fallback to None in case transcription fails ] diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index cf3603c6ff..da52be6e79 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -24,9 +24,17 @@ from open_webui.constants import ERROR_MESSAGES from open_webui.env import SRC_LOG_LEVELS +from open_webui.utils.models import ( + get_all_models, + get_filtered_models, +) +from open_webui.utils.chat import generate_chat_completion + + from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.access_control import has_access, get_users_with_access from open_webui.utils.webhook import post_webhook +from open_webui.utils.channels import extract_mentions, replace_mentions log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MODELS"]) @@ -200,14 +208,11 @@ async def send_notification(name, webui_url, channel, message, active_user_ids): users = get_users_with_access("read", channel.access_control) for user in users: - if user.id in active_user_ids: - continue - else: + if user.id not in active_user_ids: if user.settings: webhook_url = user.settings.ui.get("notifications", {}).get( "webhook_url", None ) - if webhook_url: await post_webhook( name, @@ -221,14 +226,134 @@ async def send_notification(name, webui_url, channel, message, active_user_ids): }, ) + return True -@router.post("/{id}/messages/post", response_model=Optional[MessageModel]) -async def post_new_message( - request: Request, - id: str, - form_data: MessageForm, - background_tasks: BackgroundTasks, - user=Depends(get_verified_user), + +async def model_response_handler(request, channel, message, user): + MODELS = { + model["id"]: model + for model in get_filtered_models(await get_all_models(request, user=user), user) + } + + mentions = extract_mentions(message.content) + message_content = replace_mentions(message.content) + + # check if any of the mentions are models + model_mentions = [mention for mention in mentions if mention["id_type"] == "M"] + if not model_mentions: + return False + + for mention in model_mentions: + model_id = mention["id"] + model = MODELS.get(model_id, None) + + if model: + try: + # reverse to get in chronological order + thread_messages = Messages.get_messages_by_parent_id( + channel.id, + message.parent_id if message.parent_id else message.id, + )[::-1] + + response_message, channel = await new_message_handler( + request, + channel.id, + MessageForm( + **{ + "parent_id": ( + message.parent_id if message.parent_id else message.id + ), + "content": f"", + "data": {}, + "meta": { + "model_id": model_id, + "model_name": model.get("name", model_id), + }, + } + ), + user, + ) + + thread_history = [] + message_users = {} + + for thread_message in thread_messages: + message_user = None + if thread_message.user_id not in message_users: + message_user = Users.get_user_by_id(thread_message.user_id) + message_users[thread_message.user_id] = message_user + else: + message_user = message_users[thread_message.user_id] + + if thread_message.meta and thread_message.meta.get( + "model_id", None + ): + # If the message was sent by a model, use the model name + message_model_id = thread_message.meta.get("model_id", None) + message_model = MODELS.get(message_model_id, None) + username = ( + message_model.get("name", message_model_id) + if message_model + else message_model_id + ) + else: + username = message_user.name if message_user else "Unknown" + + thread_history.append( + f"{username}: {replace_mentions(thread_message.content)}" + ) + + system_message = { + "role": "system", + "content": f"You are {model.get('name', model_id)}, an AI assistant participating in a threaded conversation. Be helpful, concise, and conversational." + + ( + f"Here's the thread history:\n\n{''.join([f'{msg}' for msg in thread_history])}\n\nContinue the conversation naturally, addressing the most recent message while being aware of the full context." + if thread_history + else "" + ), + } + + form_data = { + "model": model_id, + "messages": [ + system_message, + { + "role": "user", + "content": f"{user.name if user else 'User'}: {message_content}", + }, + ], + "stream": False, + } + + res = await generate_chat_completion( + request, + form_data=form_data, + user=user, + ) + + if res: + await update_message_by_id( + channel.id, + response_message.id, + MessageForm( + **{ + "content": res["choices"][0]["message"]["content"], + "meta": { + "done": True, + }, + } + ), + user, + ) + except Exception as e: + log.info(e) + pass + + return True + + +async def new_message_handler( + request: Request, id: str, form_data: MessageForm, user=Depends(get_verified_user) ): channel = Channels.get_channel_by_id(id) if not channel: @@ -302,11 +427,30 @@ async def post_new_message( }, to=f"channel:{channel.id}", ) + return MessageModel(**message.model_dump()), channel + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() + ) - active_user_ids = get_user_ids_from_room(f"channel:{channel.id}") - background_tasks.add_task( - send_notification, +@router.post("/{id}/messages/post", response_model=Optional[MessageModel]) +async def post_new_message( + request: Request, + id: str, + form_data: MessageForm, + background_tasks: BackgroundTasks, + user=Depends(get_verified_user), +): + + try: + message, channel = await new_message_handler(request, id, form_data, user) + active_user_ids = get_user_ids_from_room(f"channel:{channel.id}") + + async def background_handler(): + await model_response_handler(request, channel, message, user) + await send_notification( request.app.state.WEBUI_NAME, request.app.state.config.WEBUI_URL, channel, @@ -314,7 +458,12 @@ async def post_new_message( active_user_ids, ) - return MessageModel(**message.model_dump()) + background_tasks.add_task(background_handler) + + return message + + except HTTPException as e: + raise e except Exception as e: log.exception(e) raise HTTPException( diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index 6f853ab266..847368412e 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -166,7 +166,7 @@ async def import_chat(form_data: ChatImportForm, user=Depends(get_verified_user) @router.get("/search", response_model=list[ChatTitleIdResponse]) -async def search_user_chats( +def search_user_chats( text: str, page: Optional[int] = None, user=Depends(get_verified_user) ): if page is None: diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index 778fbdec27..84d8f841cf 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -120,11 +120,6 @@ def process_uploaded_file(request, file, file_path, file_item, file_metadata, us f"File type {file.content_type} is not provided, but trying to process anyway" ) process_file(request, ProcessFileForm(file_id=file_item.id), user=user) - - Files.update_file_data_by_id( - file_item.id, - {"status": "completed"}, - ) except Exception as e: log.error(f"Error processing file: {file_item.id}") Files.update_file_data_by_id( diff --git a/backend/open_webui/routers/functions.py b/backend/open_webui/routers/functions.py index 9ef6915709..202aa74ca4 100644 --- a/backend/open_webui/routers/functions.py +++ b/backend/open_webui/routers/functions.py @@ -148,6 +148,18 @@ async def sync_functions( content=function.content, ) + if hasattr(function_module, "Valves") and function.valves: + Valves = function_module.Valves + try: + Valves( + **{k: v for k, v in function.valves.items() if v is not None} + ) + except Exception as e: + log.exception( + f"Error validating valves for function {function.id}: {e}" + ) + raise e + return Functions.sync_functions(user.id, form_data.functions) except Exception as e: log.exception(f"Failed to load a function: {e}") @@ -192,6 +204,9 @@ async def create_new_function( function_cache_dir = CACHE_DIR / "functions" / form_data.id function_cache_dir.mkdir(parents=True, exist_ok=True) + if function_type == "filter" and getattr(function_module, "toggle", None): + Functions.update_function_metadata_by_id(id, {"toggle": True}) + if function: return function else: @@ -308,6 +323,9 @@ async def update_function_by_id( function = Functions.update_function_by_id(id, updated) + if function_type == "filter" and getattr(function_module, "toggle", None): + Functions.update_function_metadata_by_id(id, {"toggle": True}) + if function: return function else: diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index a4d4e3668e..05d7c68006 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -1,4 +1,6 @@ from typing import Optional +import io +import base64 from open_webui.models.models import ( ModelForm, @@ -10,12 +12,13 @@ from open_webui.models.models import ( from pydantic import BaseModel from open_webui.constants import ERROR_MESSAGES -from fastapi import APIRouter, Depends, HTTPException, Request, status +from fastapi import APIRouter, Depends, HTTPException, Request, status, Response +from fastapi.responses import FileResponse, StreamingResponse from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.access_control import has_access, has_permission -from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL +from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL, STATIC_DIR router = APIRouter() @@ -129,6 +132,39 @@ async def get_model_by_id(id: str, user=Depends(get_verified_user)): ) +########################### +# GetModelById +########################### + + +@router.get("/model/profile/image") +async def get_model_profile_image(id: str, user=Depends(get_verified_user)): + model = Models.get_model_by_id(id) + if model: + if model.meta.profile_image_url: + if model.meta.profile_image_url.startswith("http"): + return Response( + status_code=status.HTTP_302_FOUND, + headers={"Location": model.meta.profile_image_url}, + ) + elif model.meta.profile_image_url.startswith("data:image"): + try: + header, base64_data = model.meta.profile_image_url.split(",", 1) + image_data = base64.b64decode(base64_data) + image_buffer = io.BytesIO(image_data) + + return StreamingResponse( + image_buffer, + media_type="image/png", + headers={"Content-Disposition": "inline; filename=image.png"}, + ) + except Exception as e: + pass + return FileResponse(f"{STATIC_DIR}/favicon.png") + else: + return FileResponse(f"{STATIC_DIR}/favicon.png") + + ############################ # ToggleModelById ############################ diff --git a/backend/open_webui/routers/notes.py b/backend/open_webui/routers/notes.py index 375f59ff6c..dff7bc2e7f 100644 --- a/backend/open_webui/routers/notes.py +++ b/backend/open_webui/routers/notes.py @@ -62,8 +62,9 @@ class NoteTitleIdResponse(BaseModel): @router.get("/list", response_model=list[NoteTitleIdResponse]) -async def get_note_list(request: Request, user=Depends(get_verified_user)): - +async def get_note_list( + request: Request, page: Optional[int] = None, user=Depends(get_verified_user) +): if user.role != "admin" and not has_permission( user.id, "features.notes", request.app.state.config.USER_PERMISSIONS ): @@ -72,9 +73,15 @@ async def get_note_list(request: Request, user=Depends(get_verified_user)): detail=ERROR_MESSAGES.UNAUTHORIZED, ) + limit = None + skip = None + if page is not None: + limit = 60 + skip = (page - 1) * limit + notes = [ NoteTitleIdResponse(**note.model_dump()) - for note in Notes.get_notes_by_user_id(user.id, "write") + for note in Notes.get_notes_by_user_id(user.id, "write", skip=skip, limit=limit) ] return notes diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index 184f47038d..3154be2ee6 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -9,6 +9,8 @@ from aiocache import cached import requests from urllib.parse import quote +from azure.identity import DefaultAzureCredential, get_bearer_token_provider + from fastapi import Depends, HTTPException, Request, APIRouter from fastapi.responses import ( FileResponse, @@ -171,22 +173,41 @@ def get_headers_and_cookies( oauth_token = None try: - oauth_token = request.app.state.oauth_manager.get_oauth_token( - user.id, - request.cookies.get("oauth_session_id", None), - ) + if request.cookies.get("oauth_session_id", None): + oauth_token = request.app.state.oauth_manager.get_oauth_token( + user.id, + request.cookies.get("oauth_session_id", None), + ) except Exception as e: log.error(f"Error getting OAuth token: {e}") if oauth_token: token = f"{oauth_token.get('access_token', '')}" + elif auth_type in ("azure_ad", "microsoft_entra_id"): + token = get_microsoft_entra_id_access_token() + if token: headers["Authorization"] = f"Bearer {token}" return headers, cookies +def get_microsoft_entra_id_access_token(): + """ + Get Microsoft Entra ID access token using DefaultAzureCredential for Azure OpenAI. + Returns the token string or None if authentication fails. + """ + try: + token_provider = get_bearer_token_provider( + DefaultAzureCredential(), "https://cognitiveservices.azure.com/.default" + ) + return token_provider() + except Exception as e: + log.error(f"Error getting Microsoft Entra ID access token: {e}") + return None + + ########################################## # # API routes @@ -640,9 +661,12 @@ async def verify_connection( ) if api_config.get("azure", False): - headers["api-key"] = key - api_version = api_config.get("api_version", "") or "2023-03-15-preview" + # Only set api-key header if not using Azure Entra ID authentication + auth_type = api_config.get("auth_type", "bearer") + if auth_type not in ("azure_ad", "microsoft_entra_id"): + headers["api-key"] = key + api_version = api_config.get("api_version", "") or "2023-03-15-preview" async with session.get( url=f"{url}/openai/models?api-version={api_version}", headers=headers, @@ -884,7 +908,12 @@ async def generate_chat_completion( if api_config.get("azure", False): api_version = api_config.get("api_version", "2023-03-15-preview") request_url, payload = convert_to_azure_payload(url, payload, api_version) - headers["api-key"] = key + + # Only set api-key header if not using Azure Entra ID authentication + auth_type = api_config.get("auth_type", "bearer") + if auth_type not in ("azure_ad", "microsoft_entra_id"): + headers["api-key"] = key + headers["api-version"] = api_version request_url = f"{request_url}/chat/completions?api-version={api_version}" else: @@ -1057,7 +1086,12 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): if api_config.get("azure", False): api_version = api_config.get("api_version", "2023-03-15-preview") - headers["api-key"] = key + + # Only set api-key header if not using Azure Entra ID authentication + auth_type = api_config.get("auth_type", "bearer") + if auth_type not in ("azure_ad", "microsoft_entra_id"): + headers["api-key"] = key + headers["api-version"] = api_version payload = json.loads(body) diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index dd5e2d5bc4..0ddf824efa 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -1334,7 +1334,7 @@ def save_docs_to_vector_db( ) return True - log.info(f"adding to collection {collection_name}") + log.info(f"generating embeddings for {collection_name}") embedding_function = get_embedding_function( request.app.state.config.RAG_EMBEDDING_ENGINE, request.app.state.config.RAG_EMBEDDING_MODEL, @@ -1370,6 +1370,7 @@ def save_docs_to_vector_db( prefix=RAG_EMBEDDING_CONTENT_PREFIX, user=user, ) + log.info(f"embeddings generated {len(embeddings)} for {len(texts)} items") items = [ { @@ -1381,11 +1382,13 @@ def save_docs_to_vector_db( for idx, text in enumerate(texts) ] + log.info(f"adding to collection {collection_name}") VECTOR_DB_CLIENT.insert( collection_name=collection_name, items=items, ) + log.info(f"added {len(items)} items to collection {collection_name}") return True except Exception as e: log.exception(e) @@ -1544,13 +1547,20 @@ def process_file( log.debug(f"text_content: {text_content}") Files.update_file_data_by_id( file.id, - {"status": "completed", "content": text_content}, + {"content": text_content}, ) - hash = calculate_sha256_string(text_content) Files.update_file_hash_by_id(file.id, hash) - if not request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL: + if request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL: + Files.update_file_data_by_id(file.id, {"status": "completed"}) + return { + "status": True, + "collection_name": None, + "filename": file.filename, + "content": text_content, + } + else: try: result = save_docs_to_vector_db( request, @@ -1564,6 +1574,7 @@ def process_file( add=(True if form_data.collection_name else False), user=user, ) + log.info(f"added {len(docs)} items to collection {collection_name}") if result: Files.update_file_metadata_by_id( @@ -1573,21 +1584,21 @@ def process_file( }, ) + Files.update_file_data_by_id( + file.id, + {"status": "completed"}, + ) + return { "status": True, "collection_name": collection_name, "filename": file.filename, "content": text_content, } + else: + raise Exception("Error saving document to vector database") except Exception as e: raise e - else: - return { - "status": True, - "collection_name": None, - "filename": file.filename, - "content": text_content, - } except Exception as e: log.exception(e) diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index 5b331dce73..9a0f8c6aaf 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -18,6 +18,7 @@ from open_webui.models.users import ( UserModel, UserListResponse, UserInfoListResponse, + UserIdNameListResponse, UserRoleUpdateForm, Users, UserSettings, @@ -100,6 +101,23 @@ async def get_all_users( return Users.get_users() +@router.get("/search", response_model=UserIdNameListResponse) +async def search_users( + query: Optional[str] = None, + user=Depends(get_verified_user), +): + limit = PAGE_ITEM_COUNT + + page = 1 # Always return the first page for search + skip = (page - 1) * limit + + filter = {} + if query: + filter["query"] = query + + return Users.get_users(filter=filter, skip=skip, limit=limit) + + ############################ # User Groups ############################ diff --git a/backend/open_webui/utils/access_control.py b/backend/open_webui/utils/access_control.py index 1529773c44..6215a6ac22 100644 --- a/backend/open_webui/utils/access_control.py +++ b/backend/open_webui/utils/access_control.py @@ -130,9 +130,10 @@ def has_access( # Get all users with access to a resource def get_users_with_access( type: str = "write", access_control: Optional[dict] = None -) -> List[UserModel]: +) -> list[UserModel]: if access_control is None: - return Users.get_users() + result = Users.get_users() + return result.get("users", []) permission_access = access_control.get(type, {}) permitted_group_ids = permission_access.get("group_ids", []) diff --git a/backend/open_webui/utils/channels.py b/backend/open_webui/utils/channels.py new file mode 100644 index 0000000000..312b5ea24c --- /dev/null +++ b/backend/open_webui/utils/channels.py @@ -0,0 +1,31 @@ +import re + + +def extract_mentions(message: str, triggerChar: str = "@"): + # Escape triggerChar in case it's a regex special character + triggerChar = re.escape(triggerChar) + pattern = rf"<{triggerChar}([A-Z]):([^|>]+)" + + matches = re.findall(pattern, message) + return [{"id_type": id_type, "id": id_value} for id_type, id_value in matches] + + +def replace_mentions(message: str, triggerChar: str = "@", use_label: bool = True): + """ + Replace mentions in the message with either their label (after the pipe `|`) + or their id if no label exists. + + Example: + "<@M:gpt-4.1|GPT-4>" -> "GPT-4" (if use_label=True) + "<@M:gpt-4.1|GPT-4>" -> "gpt-4.1" (if use_label=False) + """ + # Escape triggerChar + triggerChar = re.escape(triggerChar) + + def replacer(match): + id_type, id_value, label = match.groups() + return label if use_label and label else id_value + + # Regex captures: idType, id, optional label + pattern = rf"<{triggerChar}([A-Z]):([^|>]+)(?:\|([^>]+))?>" + return re.sub(pattern, replacer, message) diff --git a/backend/open_webui/utils/filter.py b/backend/open_webui/utils/filter.py index 1986e55b64..663b4e3fb7 100644 --- a/backend/open_webui/utils/filter.py +++ b/backend/open_webui/utils/filter.py @@ -127,8 +127,10 @@ async def process_filter_functions( raise e # Handle file cleanup for inlet - if skip_files and "files" in form_data.get("metadata", {}): - del form_data["files"] - del form_data["metadata"]["files"] + if skip_files: + if "files" in form_data.get("metadata", {}): + del form_data["metadata"]["files"] + if "files" in form_data: + del form_data["files"] return form_data, {} diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index ae2c96c6da..97f19dcded 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -817,10 +817,11 @@ async def process_chat_payload(request, form_data, user, metadata, model): oauth_token = None try: - oauth_token = request.app.state.oauth_manager.get_oauth_token( - user.id, - request.cookies.get("oauth_session_id", None), - ) + if request.cookies.get("oauth_session_id", None): + oauth_token = request.app.state.oauth_manager.get_oauth_token( + user.id, + request.cookies.get("oauth_session_id", None), + ) except Exception as e: log.error(f"Error getting OAuth token: {e}") @@ -1130,11 +1131,11 @@ async def process_chat_response( request, response, form_data, user, metadata, model, events, tasks ): async def background_tasks_handler(): - message_map = Chats.get_messages_by_chat_id(metadata["chat_id"]) - message = message_map.get(metadata["message_id"]) if message_map else None + messages_map = Chats.get_messages_map_by_chat_id(metadata["chat_id"]) + message = messages_map.get(metadata["message_id"]) if messages_map else None if message: - message_list = get_message_list(message_map, metadata["message_id"]) + message_list = get_message_list(messages_map, metadata["message_id"]) # Remove details tags and files from the messages. # as get_message_list creates a new list, it does not affect @@ -1496,10 +1497,11 @@ async def process_chat_response( oauth_token = None try: - oauth_token = request.app.state.oauth_manager.get_oauth_token( - user.id, - request.cookies.get("oauth_session_id", None), - ) + if request.cookies.get("oauth_session_id", None): + oauth_token = request.app.state.oauth_manager.get_oauth_token( + user.id, + request.cookies.get("oauth_session_id", None), + ) except Exception as e: log.error(f"Error getting OAuth token: {e}") @@ -2029,6 +2031,20 @@ async def process_chat_response( ) else: choices = data.get("choices", []) + + # 17421 + usage = data.get("usage", {}) or {} + usage.update(data.get("timings", {})) # llama.cpp + if usage: + await event_emitter( + { + "type": "chat:completion", + "data": { + "usage": usage, + }, + } + ) + if not choices: error = data.get("error", {}) if error: @@ -2040,20 +2056,6 @@ async def process_chat_response( }, } ) - usage = data.get("usage", {}) - usage.update( - data.get("timing", {}) - ) # llama.cpp - - if usage: - await event_emitter( - { - "type": "chat:completion", - "data": { - "usage": usage, - }, - } - ) continue delta = choices[0].get("delta", {}) diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index 82729c34e0..370cf26c48 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -26,7 +26,7 @@ def deep_update(d, u): return d -def get_message_list(messages, message_id): +def get_message_list(messages_map, message_id): """ Reconstructs a list of messages in order up to the specified message_id. @@ -36,11 +36,11 @@ def get_message_list(messages, message_id): """ # Handle case where messages is None - if not messages: + if not messages_map: return [] # Return empty list instead of None to prevent iteration errors # Find the message by its id - current_message = messages.get(message_id) + current_message = messages_map.get(message_id) if not current_message: return [] # Return empty list instead of None to prevent iteration errors @@ -53,7 +53,7 @@ def get_message_list(messages, message_id): 0, current_message ) # Insert the message at the beginning of the list parent_id = current_message.get("parentId") # Use .get() for safety - current_message = messages.get(parent_id) if parent_id else None + current_message = messages_map.get(parent_id) if parent_id else None return message_list diff --git a/backend/open_webui/utils/models.py b/backend/open_webui/utils/models.py index b713b84307..7e69661f56 100644 --- a/backend/open_webui/utils/models.py +++ b/backend/open_webui/utils/models.py @@ -22,10 +22,11 @@ from open_webui.utils.access_control import has_access from open_webui.config import ( + BYPASS_ADMIN_ACCESS_CONTROL, DEFAULT_ARENA_MODEL, ) -from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL +from open_webui.env import BYPASS_MODEL_ACCESS_CONTROL, SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL from open_webui.models.users import UserModel @@ -332,3 +333,40 @@ def check_model_access(user, model): ) ): raise Exception("Model not found") + + +def get_filtered_models(models, user): + # Filter out models that the user does not have access to + if ( + user.role == "user" + or (user.role == "admin" and not BYPASS_ADMIN_ACCESS_CONTROL) + ) and not BYPASS_MODEL_ACCESS_CONTROL: + filtered_models = [] + for model in models: + if model.get("arena"): + if has_access( + user.id, + type="read", + access_control=model.get("info", {}) + .get("meta", {}) + .get("access_control", {}), + ): + filtered_models.append(model) + continue + + model_info = Models.get_model_by_id(model["id"]) + if model_info: + if ( + (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) + or user.id == model_info.user_id + or has_access( + user.id, + type="read", + access_control=model_info.access_control, + ) + ): + filtered_models.append(model) + + return filtered_models + else: + return models diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 7eedc30c31..9090c38ce5 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -602,6 +602,12 @@ class OAuthManager: or (auth_manager_config.OAUTH_USERNAME_CLAIM not in user_data) ): user_data: UserInfo = await client.userinfo(token=token) + if ( + provider == "feishu" + and isinstance(user_data, dict) + and "data" in user_data + ): + user_data = user_data["data"] if not user_data: log.warning(f"OAuth callback failed, user data is missing: {token}") raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) diff --git a/backend/open_webui/utils/telemetry/metrics.py b/backend/open_webui/utils/telemetry/metrics.py index 75c13ccc0a..c7b47c0231 100644 --- a/backend/open_webui/utils/telemetry/metrics.py +++ b/backend/open_webui/utils/telemetry/metrics.py @@ -163,20 +163,27 @@ def setup_metrics(app: FastAPI, resource: Resource) -> None: @app.middleware("http") async def _metrics_middleware(request: Request, call_next): start_time = time.perf_counter() - response = await call_next(request) - elapsed_ms = (time.perf_counter() - start_time) * 1000.0 - # Route template e.g. "/items/{item_id}" instead of real path. - route = request.scope.get("route") - route_path = getattr(route, "path", request.url.path) + status_code = None + try: + response = await call_next(request) + status_code = getattr(response, "status_code", 500) + return response + except Exception: + status_code = 500 + raise + finally: + elapsed_ms = (time.perf_counter() - start_time) * 1000.0 - attrs: Dict[str, str | int] = { - "http.method": request.method, - "http.route": route_path, - "http.status_code": response.status_code, - } + # Route template e.g. "/items/{item_id}" instead of real path. + route = request.scope.get("route") + route_path = getattr(route, "path", request.url.path) - request_counter.add(1, attrs) - duration_histogram.record(elapsed_ms, attrs) + attrs: Dict[str, str | int] = { + "http.method": request.method, + "http.route": route_path, + "http.status_code": status_code, + } - return response + request_counter.add(1, attrs) + duration_histogram.record(elapsed_ms, attrs) diff --git a/package-lock.json b/package-lock.json index 48122812b8..b9b9be7a2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.6.28", + "version": "0.6.29", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.6.28", + "version": "0.6.29", "dependencies": { "@azure/msal-browser": "^4.5.0", "@codemirror/lang-javascript": "^6.2.2", @@ -37,6 +37,7 @@ "@tiptap/extensions": "^3.0.7", "@tiptap/pm": "^3.0.7", "@tiptap/starter-kit": "^3.0.7", + "@tiptap/suggestion": "^3.4.2", "@xyflow/svelte": "^0.1.19", "async": "^3.2.5", "bits-ui": "^0.21.15", @@ -86,7 +87,6 @@ "socket.io-client": "^4.2.0", "sortablejs": "^1.15.6", "svelte-sonner": "^0.3.19", - "svelte-tiptap": "^3.0.0", "tippy.js": "^6.3.7", "turndown": "^7.2.0", "turndown-plugin-gfm": "^1.0.2", @@ -3856,18 +3856,17 @@ } }, "node_modules/@tiptap/suggestion": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-3.0.9.tgz", - "integrity": "sha512-irthqfUybezo3IwR6AXvyyTOtkzwfvvst58VXZtTnR1nN6NEcrs3TQoY3bGKGbN83bdiquKh6aU2nLnZfAhoXg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-3.4.2.tgz", + "integrity": "sha512-sljtfiDtdAsbPOwrXrFGf64D6sXUjeU3Iz5v3TvN7TVJKozkZ/gaMkPRl+WC1CGwC6BnzQVDBEEa1e+aApV0mA==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.0.9", - "@tiptap/pm": "^3.0.9" + "@tiptap/core": "^3.4.2", + "@tiptap/pm": "^3.4.2" } }, "node_modules/@tiptap/y-tiptap": { @@ -12503,26 +12502,6 @@ "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.1" } }, - "node_modules/svelte-tiptap": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/svelte-tiptap/-/svelte-tiptap-3.0.0.tgz", - "integrity": "sha512-digFHOJe16RX0HIU+u8hOaCS9sIgktTpYHSF9yJ6dgxPv/JWJdYCdwoX65lcHitFhhCG7xnolJng6PJa9M9h3w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "peerDependencies": { - "@floating-ui/dom": "^1.0.0", - "@tiptap/core": "^3.0.0", - "@tiptap/extension-bubble-menu": "^3.0.0", - "@tiptap/extension-floating-menu": "^3.0.0", - "@tiptap/pm": "^3.0.0", - "svelte": "^5.0.0" - } - }, "node_modules/svelte/node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", diff --git a/package.json b/package.json index 054baf39c6..c5068f0366 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.6.28", + "version": "0.6.29", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", @@ -81,6 +81,7 @@ "@tiptap/extensions": "^3.0.7", "@tiptap/pm": "^3.0.7", "@tiptap/starter-kit": "^3.0.7", + "@tiptap/suggestion": "^3.4.2", "@xyflow/svelte": "^0.1.19", "async": "^3.2.5", "bits-ui": "^0.21.15", @@ -130,7 +131,6 @@ "socket.io-client": "^4.2.0", "sortablejs": "^1.15.6", "svelte-sonner": "^0.3.19", - "svelte-tiptap": "^3.0.0", "tippy.js": "^6.3.7", "turndown": "^7.2.0", "turndown-plugin-gfm": "^1.0.2", diff --git a/pyproject.toml b/pyproject.toml index a0e7e4a095..4bbaf85b6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,18 +56,11 @@ dependencies = [ "fake-useragent==2.2.0", "chromadb==1.0.20", - "pymilvus==2.5.0", - "qdrant-client==1.14.3", "opensearch-py==2.8.0", - "playwright==1.49.1", - "elasticsearch==9.1.0", - "pinecone==6.0.2", - "oracledb==3.2.0", - + "transformers", "sentence-transformers==4.1.0", "accelerate", - "colbert-ai==0.2.21", "pyarrow==20.0.0", "einops==0.8.1", @@ -154,6 +147,15 @@ all = [ "docker~=7.1.0", "pytest~=8.3.2", "pytest-docker~=3.1.1", + "playwright==1.49.1", + "elasticsearch==9.1.0", + + "qdrant-client==1.14.3", + "pymilvus==2.5.0", + "pinecone==6.0.2", + "oracledb==3.2.0", + + "colbert-ai==0.2.21", ] [project.scripts] diff --git a/src/app.css b/src/app.css index c48914febf..af030d8350 100644 --- a/src/app.css +++ b/src/app.css @@ -70,23 +70,23 @@ textarea::placeholder { } .input-prose { - @apply prose dark:prose-invert prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-1 prose-img:my-1 prose-headings:my-2 prose-pre:my-0 prose-table:my-1 prose-blockquote:my-0 prose-ul:my-1 prose-ol:my-1 prose-li:my-0.5 whitespace-pre-line; + @apply prose dark:prose-invert prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-50 prose-hr:dark:border-gray-850 prose-p:my-1 prose-img:my-1 prose-headings:my-2 prose-pre:my-0 prose-table:my-1 prose-blockquote:my-0 prose-ul:my-1 prose-ol:my-1 prose-li:my-0.5 whitespace-pre-line; } .input-prose-sm { - @apply prose dark:prose-invert prose-headings:font-medium prose-h1:text-2xl prose-h2:text-xl prose-h3:text-lg prose-hr:my-4 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-1 prose-img:my-1 prose-headings:my-2 prose-pre:my-0 prose-table:my-1 prose-blockquote:my-0 prose-ul:my-1 prose-ol:my-1 prose-li:my-1 whitespace-pre-line text-sm; + @apply prose dark:prose-invert prose-headings:font-medium prose-h1:text-2xl prose-h2:text-xl prose-h3:text-lg prose-hr:my-4 prose-hr:border-gray-50 prose-hr:dark:border-gray-850 prose-p:my-1 prose-img:my-1 prose-headings:my-2 prose-pre:my-0 prose-table:my-1 prose-blockquote:my-0 prose-ul:my-1 prose-ol:my-1 prose-li:my-1 whitespace-pre-line text-sm; } .markdown-prose { - @apply prose dark:prose-invert prose-blockquote:border-s-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-s-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line; + @apply prose dark:prose-invert prose-blockquote:border-s-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-s-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-50 prose-hr:dark:border-gray-850 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line; } .markdown-prose-sm { - @apply text-sm prose dark:prose-invert prose-blockquote:border-s-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-s-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-2 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line; + @apply text-sm prose dark:prose-invert prose-blockquote:border-s-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-s-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-2 prose-hr:border-gray-50 prose-hr:dark:border-gray-850 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line; } .markdown-prose-xs { - @apply text-xs prose dark:prose-invert prose-blockquote:border-s-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-s-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-0.5 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line; + @apply text-xs prose dark:prose-invert prose-blockquote:border-s-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-s-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-0.5 prose-hr:border-gray-50 prose-hr:dark:border-gray-850 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line; } .markdown a { @@ -409,17 +409,33 @@ input[type='number'] { } } -.tiptap .mention { +.mention { border-radius: 0.4rem; box-decoration-break: clone; padding: 0.1rem 0.3rem; - @apply text-blue-900 dark:text-blue-100 bg-blue-300/20 dark:bg-blue-500/20; + @apply text-sky-800 dark:text-sky-200 bg-sky-300/15 dark:bg-sky-500/15; } -.tiptap .mention::after { +.mention::after { content: '\200B'; } +.tiptap .suggestion { + border-radius: 0.4rem; + box-decoration-break: clone; + padding: 0.1rem 0.3rem; + @apply text-sky-800 dark:text-sky-200 bg-sky-300/15 dark:bg-sky-500/15; +} + +.tiptap .suggestion::after { + content: '\200B'; +} + +.tiptap .suggestion.is-empty::after { + content: '\00A0'; + border-bottom: 1px dotted rgba(31, 41, 55, 0.12); +} + .input-prose .tiptap ul[data-type='taskList'] { list-style: none; margin-left: 0; diff --git a/src/app.html b/src/app.html index f7167d42f2..6c1c362005 100644 --- a/src/app.html +++ b/src/app.html @@ -23,8 +23,6 @@ href="/static/apple-touch-icon.png" crossorigin="use-credentials" /> - - - - + diff --git a/src/lib/apis/notes/index.ts b/src/lib/apis/notes/index.ts index 965c4217ed..61794f6766 100644 --- a/src/lib/apis/notes/index.ts +++ b/src/lib/apis/notes/index.ts @@ -91,10 +91,15 @@ export const getNotes = async (token: string = '', raw: boolean = false) => { return grouped; }; -export const getNoteList = async (token: string = '') => { +export const getNoteList = async (token: string = '', page: number | null = null) => { let error = null; + const searchParams = new URLSearchParams(); - const res = await fetch(`${WEBUI_API_BASE_URL}/notes/list`, { + if (page !== null) { + searchParams.append('page', `${page}`); + } + + const res = await fetch(`${WEBUI_API_BASE_URL}/notes/list?${searchParams.toString()}`, { method: 'GET', headers: { Accept: 'application/json', diff --git a/src/lib/apis/users/index.ts b/src/lib/apis/users/index.ts index bdb44f2627..ac057359a5 100644 --- a/src/lib/apis/users/index.ts +++ b/src/lib/apis/users/index.ts @@ -194,6 +194,34 @@ export const getAllUsers = async (token: string) => { return res; }; +export const searchUsers = async (token: string, query: string) => { + let error = null; + let res = null; + + res = await fetch(`${WEBUI_API_BASE_URL}/users/search?query=${encodeURIComponent(query)}`, { + 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.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const getUserSettings = async (token: string) => { let error = null; const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/settings`, { diff --git a/src/lib/components/AddConnectionModal.svelte b/src/lib/components/AddConnectionModal.svelte index fb4da3175f..240df839a8 100644 --- a/src/lib/components/AddConnectionModal.svelte +++ b/src/lib/components/AddConnectionModal.svelte @@ -122,7 +122,7 @@ return; } - if (!key) { + if (!key && !['azure_ad', 'microsoft_entra_id'].includes(auth_type)) { loading = false; toast.error($i18n.t('Key is required')); @@ -331,6 +331,9 @@ {#if !direct} + {#if azure} + + {/if} {/if} {/if} @@ -361,6 +364,12 @@ > {$i18n.t('Forwards system user OAuth access token to authenticate')} + {:else if ['azure_ad', 'microsoft_entra_id'].includes(auth_type)} +