diff --git a/backend/open_webui/functions.py b/backend/open_webui/functions.py index af6dd1ce1a..d102263cb3 100644 --- a/backend/open_webui/functions.py +++ b/backend/open_webui/functions.py @@ -239,7 +239,7 @@ async def generate_function_chat_completion( oauth_token = None try: if request.cookies.get("oauth_session_id", None): - oauth_token = request.app.state.oauth_manager.get_oauth_token( + oauth_token = await request.app.state.oauth_manager.get_oauth_token( user.id, request.cookies.get("oauth_session_id", None), ) diff --git a/backend/open_webui/routers/ollama.py b/backend/open_webui/routers/ollama.py index 8dadf3523a..bf11ffa0dd 100644 --- a/backend/open_webui/routers/ollama.py +++ b/backend/open_webui/routers/ollama.py @@ -1694,25 +1694,27 @@ async def download_file_stream( yield f'data: {{"progress": {progress}, "completed": {current_size}, "total": {total_size}}}\n\n' if done: - file.seek(0) - chunk_size = 1024 * 1024 * 2 - hashed = calculate_sha256(file, chunk_size) - file.seek(0) + file.close() - url = f"{ollama_url}/api/blobs/sha256:{hashed}" - response = requests.post(url, data=file) + with open(file_path, "rb") as file: + chunk_size = 1024 * 1024 * 2 + hashed = calculate_sha256(file, chunk_size) - if response.ok: - res = { - "done": done, - "blob": f"sha256:{hashed}", - "name": file_name, - } - os.remove(file_path) + url = f"{ollama_url}/api/blobs/sha256:{hashed}" + with requests.Session() as session: + response = session.post(url, data=file, timeout=30) - yield f"data: {json.dumps(res)}\n\n" - else: - raise "Ollama: Could not create blob, Please try again." + if response.ok: + res = { + "done": done, + "blob": f"sha256:{hashed}", + "name": file_name, + } + os.remove(file_path) + + yield f"data: {json.dumps(res)}\n\n" + else: + raise "Ollama: Could not create blob, Please try again." # url = "https://huggingface.co/TheBloke/stablelm-zephyr-3b-GGUF/resolve/main/stablelm-zephyr-3b.Q2_K.gguf" diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index 3154be2ee6..e8865b90a0 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -121,7 +121,7 @@ def openai_reasoning_model_handler(payload): return payload -def get_headers_and_cookies( +async def get_headers_and_cookies( request: Request, url, key=None, @@ -174,7 +174,7 @@ def get_headers_and_cookies( oauth_token = None try: if request.cookies.get("oauth_session_id", None): - oauth_token = request.app.state.oauth_manager.get_oauth_token( + oauth_token = await request.app.state.oauth_manager.get_oauth_token( user.id, request.cookies.get("oauth_session_id", None), ) @@ -305,7 +305,7 @@ async def speech(request: Request, user=Depends(get_verified_user)): request.app.state.config.OPENAI_API_CONFIGS.get(url, {}), # Legacy support ) - headers, cookies = get_headers_and_cookies( + headers, cookies = await get_headers_and_cookies( request, url, key, api_config, user=user ) @@ -570,7 +570,7 @@ async def get_models( timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST), ) as session: try: - headers, cookies = get_headers_and_cookies( + headers, cookies = await get_headers_and_cookies( request, url, key, api_config, user=user ) @@ -656,7 +656,7 @@ async def verify_connection( timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST), ) as session: try: - headers, cookies = get_headers_and_cookies( + headers, cookies = await get_headers_and_cookies( request, url, key, api_config, user=user ) @@ -901,7 +901,7 @@ async def generate_chat_completion( convert_logit_bias_input_to_json(payload["logit_bias"]) ) - headers, cookies = get_headers_and_cookies( + headers, cookies = await get_headers_and_cookies( request, url, key, api_config, metadata, user=user ) @@ -1010,7 +1010,9 @@ async def embeddings(request: Request, form_data: dict, user): session = None streaming = False - headers, cookies = get_headers_and_cookies(request, url, key, api_config, user=user) + headers, cookies = await get_headers_and_cookies( + request, url, key, api_config, user=user + ) try: session = aiohttp.ClientSession(trust_env=True) r = await session.request( @@ -1080,7 +1082,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): streaming = False try: - headers, cookies = get_headers_and_cookies( + headers, cookies = await get_headers_and_cookies( request, url, key, api_config, user=user ) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 97f19dcded..3cd7d3a6e8 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -20,6 +20,7 @@ from concurrent.futures import ThreadPoolExecutor from fastapi import Request, HTTPException +from fastapi.responses import HTMLResponse from starlette.responses import Response, StreamingResponse, JSONResponse @@ -818,7 +819,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): oauth_token = None try: if request.cookies.get("oauth_session_id", None): - oauth_token = request.app.state.oauth_manager.get_oauth_token( + oauth_token = await request.app.state.oauth_manager.get_oauth_token( user.id, request.cookies.get("oauth_session_id", None), ) @@ -1498,7 +1499,7 @@ async def process_chat_response( oauth_token = None try: if request.cookies.get("oauth_session_id", None): - oauth_token = request.app.state.oauth_manager.get_oauth_token( + oauth_token = await request.app.state.oauth_manager.get_oauth_token( user.id, request.cookies.get("oauth_session_id", None), ) @@ -1581,7 +1582,8 @@ async def process_chat_response( break if tool_result is not None: - tool_calls_display_content = f'{tool_calls_display_content}
\nTool Executed\n
\n' + tool_result_embeds = result.get("embeds", "") + tool_calls_display_content = f'{tool_calls_display_content}
\nTool Executed\n
\n' else: tool_calls_display_content = f'{tool_calls_display_content}
\nExecuting...\n
\n' @@ -2402,6 +2404,75 @@ async def process_chat_response( except Exception as e: tool_result = str(e) + tool_result_embeds = [] + + if isinstance(tool_result, HTMLResponse): + content_disposition = tool_result.headers.get( + "Content-Disposition", "" + ) + if "inline" in content_disposition: + content = tool_result.body.decode("utf-8") + tool_result_embeds.append(content) + + if 200 <= tool_result.status_code < 300: + tool_result = { + "status": "success", + "code": "ui_component", + "message": "Embedded UI result is active and visible to the user.", + } + elif 400 <= tool_result.status_code < 500: + tool_result = { + "status": "error", + "code": "ui_component", + "message": f"Client error {tool_result.status_code} from embedded UI result.", + } + elif 500 <= tool_result.status_code < 600: + tool_result = { + "status": "error", + "code": "ui_component", + "message": f"Server error {tool_result.status_code} from embedded UI result.", + } + else: + tool_result = { + "status": "error", + "code": "ui_component", + "message": f"Unexpected status code {tool_result.status_code} from embedded UI result.", + } + else: + tool_result = tool_result.body.decode("utf-8") + + elif tool.get("type") == "external" and isinstance( + tool_result, tuple + ): + tool_result, tool_response_headers = tool_result + + if tool_response_headers: + content_disposition = tool_response_headers.get( + "Content-Disposition", "" + ) + + if "inline" in content_disposition: + content_type = tool_response_headers.get( + "Content-Type", "" + ) + location = tool_response_headers.get("Location", "") + + if "text/html" in content_type: + # Display as iframe embed + tool_result_embeds.append(tool_result) + tool_result = { + "status": "success", + "code": "ui_component", + "message": "Embedded UI result is active and visible to the user.", + } + elif location: + tool_result_embeds.append(location) + tool_result = { + "status": "success", + "code": "ui_component", + "message": "Embedded UI result is active and visible to the user.", + } + tool_result_files = [] if isinstance(tool_result, list): for item in tool_result: @@ -2426,6 +2497,11 @@ async def process_chat_response( if tool_result_files else {} ), + **( + {"embeds": tool_result_embeds} + if tool_result_embeds + else {} + ), } ) diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 9090c38ce5..ee3ba79990 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -157,7 +157,7 @@ class OAuthManager: ) return None - def get_oauth_token( + async def get_oauth_token( self, user_id: str, session_id: str, force_refresh: bool = False ): """ @@ -186,7 +186,7 @@ class OAuthManager: log.debug( f"Token refresh needed for user {user_id}, provider {session.provider}" ) - refreshed_token = self._refresh_token(session) + refreshed_token = await self._refresh_token(session) if refreshed_token: return refreshed_token else: diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py index bc90dd9d74..63df47700f 100644 --- a/backend/open_webui/utils/tools.py +++ b/backend/open_webui/utils/tools.py @@ -171,6 +171,8 @@ async def get_tools( "tool_id": tool_id, "callable": callable, "spec": spec, + # Misc info + "type": "external", } # Handle function name collisions @@ -646,7 +648,7 @@ async def execute_tool_server( name: str, params: Dict[str, Any], server_data: Dict[str, Any], -) -> Any: +) -> Tuple[Dict[str, Any], Optional[Dict[str, Any]]]: error = None try: openapi = server_data.get("openapi", {}) @@ -718,6 +720,7 @@ async def execute_tool_server( headers=headers, cookies=cookies, ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, + allow_redirects=False, ) as response: if response.status >= 400: text = await response.text() @@ -728,13 +731,15 @@ async def execute_tool_server( except Exception: response_data = await response.text() - return response_data + response_headers = response.headers + return (response_data, response_headers) else: async with request_method( final_url, headers=headers, cookies=cookies, ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, + allow_redirects=False, ) as response: if response.status >= 400: text = await response.text() @@ -745,12 +750,13 @@ async def execute_tool_server( except Exception: response_data = await response.text() - return response_data + response_headers = response.headers + return (response_data, response_headers) except Exception as err: error = str(err) log.exception(f"API Request Error: {error}") - return {"error": error} + return ({"error": error}, None) def get_tool_server_url(url: Optional[str], path: str) -> str: diff --git a/package-lock.json b/package-lock.json index 3909c68604..9bc83bd7d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "@tiptap/starter-kit": "^3.0.7", "@tiptap/suggestion": "^3.4.2", "@xyflow/svelte": "^0.1.19", + "alpinejs": "^3.15.0", "async": "^3.2.5", "bits-ui": "^0.21.15", "chart.js": "^4.5.0", @@ -4569,6 +4570,21 @@ "@types/estree": "^1.0.0" } }, + "node_modules/@vue/reactivity": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", + "integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.1.5" + } + }, + "node_modules/@vue/shared": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", + "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==", + "license": "MIT" + }, "node_modules/@webreflection/fetch": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/@webreflection/fetch/-/fetch-0.1.5.tgz", @@ -4672,6 +4688,15 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/alpinejs": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.0.tgz", + "integrity": "sha512-lpokA5okCF1BKh10LG8YjqhfpxyHBk4gE7boIgVHltJzYoM7O9nK3M7VlntLEJGsVmu7U/RzUWajmHREGT38Eg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "~3.1.1" + } + }, "node_modules/amator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/amator/-/amator-1.1.0.tgz", diff --git a/package.json b/package.json index f577cfdcb6..aa3c6d644c 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "@tiptap/starter-kit": "^3.0.7", "@tiptap/suggestion": "^3.4.2", "@xyflow/svelte": "^0.1.19", + "alpinejs": "^3.15.0", "async": "^3.2.5", "bits-ui": "^0.21.15", "chart.js": "^4.5.0", diff --git a/src/app.css b/src/app.css index af030d8350..f52b58355a 100644 --- a/src/app.css +++ b/src/app.css @@ -116,7 +116,7 @@ li p { ::-webkit-scrollbar-thumb { --tw-border-opacity: 1; - background-color: rgba(215, 215, 215, 0.8); + background-color: rgba(215, 215, 215, 0.6); border-color: rgba(255, 255, 255, var(--tw-border-opacity)); border-radius: 9999px; border-width: 1px; @@ -124,12 +124,12 @@ li p { /* Dark theme scrollbar styles */ .dark ::-webkit-scrollbar-thumb { - background-color: rgba(67, 67, 67, 0.8); /* Darker color for dark theme */ + background-color: rgba(67, 67, 67, 0.6); /* Darker color for dark theme */ border-color: rgba(0, 0, 0, var(--tw-border-opacity)); } ::-webkit-scrollbar { - height: 0.6rem; + height: 0.4rem; width: 0.4rem; } diff --git a/src/lib/components/ChangelogModal.svelte b/src/lib/components/ChangelogModal.svelte index dec152b21b..d0d1c638a7 100644 --- a/src/lib/components/ChangelogModal.svelte +++ b/src/lib/components/ChangelogModal.svelte @@ -29,9 +29,9 @@ -
+
-
+
{$i18n.t("What's New in")} {$WEBUI_NAME} @@ -51,7 +51,7 @@
{$i18n.t('Release Notes')}
-
+
v{WEBUI_VERSION}
@@ -59,7 +59,7 @@
-
+
{#if changelog} {#each Object.keys(changelog) as version} @@ -68,20 +68,20 @@ v{version} - {changelog[version].date}
-
+
{#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section}
{section}
diff --git a/src/lib/components/admin/Users/UserList.svelte b/src/lib/components/admin/Users/UserList.svelte index 5a844cefb5..ebb3687e7d 100644 --- a/src/lib/components/admin/Users/UserList.svelte +++ b/src/lib/components/admin/Users/UserList.svelte @@ -222,7 +222,7 @@
- +
{ uploadFilesHandler(); }} @@ -64,7 +64,7 @@ { screenCaptureHandler(); }} diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index e987733221..b2dc86dfe0 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -465,6 +465,15 @@ return; } + if (event.data.type === 'action:submit') { + console.debug(event.data.text); + + if (prompt !== '') { + await tick(); + submitPrompt(prompt); + } + } + // Replace with your iframe's origin if (event.data.type === 'input:prompt') { console.debug(event.data.text); @@ -477,15 +486,6 @@ } } - if (event.data.type === 'action:submit') { - console.debug(event.data.text); - - if (prompt !== '') { - await tick(); - submitPrompt(prompt); - } - } - if (event.data.type === 'input:prompt:submit') { console.debug(event.data.text); diff --git a/src/lib/components/chat/MessageInput/InputMenu.svelte b/src/lib/components/chat/MessageInput/InputMenu.svelte index 977e18f91f..a8a44944c3 100644 --- a/src/lib/components/chat/MessageInput/InputMenu.svelte +++ b/src/lib/components/chat/MessageInput/InputMenu.svelte @@ -120,7 +120,7 @@ className="w-full" > { @@ -144,7 +144,7 @@ className="w-full" > { @@ -176,7 +176,7 @@ className="w-full" >