diff --git a/backend/open_webui/routers/functions.py b/backend/open_webui/routers/functions.py index aec75f75a..248ec42fa 100644 --- a/backend/open_webui/routers/functions.py +++ b/backend/open_webui/routers/functions.py @@ -1,5 +1,8 @@ import os +import re + import logging +import aiohttp from pathlib import Path from typing import Optional @@ -15,6 +18,8 @@ from open_webui.constants import ERROR_MESSAGES from fastapi import APIRouter, Depends, HTTPException, Request, status from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.env import SRC_LOG_LEVELS +from pydantic import BaseModel, HttpUrl + log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MAIN"]) @@ -42,6 +47,77 @@ async def get_functions(user=Depends(get_admin_user)): return Functions.get_functions() +############################ +# LoadFunctionFromLink +############################ + + +class LoadUrlForm(BaseModel): + url: HttpUrl + + +def github_url_to_raw_url(url: str) -> str: + # Handle 'tree' (folder) URLs (add main.py at the end) + m1 = re.match(r"https://github\.com/([^/]+)/([^/]+)/tree/([^/]+)/(.*)", url) + if m1: + org, repo, branch, path = m1.groups() + return f"https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path.rstrip('/')}/main.py" + + # Handle 'blob' (file) URLs + m2 = re.match(r"https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.*)", url) + if m2: + org, repo, branch, path = m2.groups() + return ( + f"https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path}" + ) + + # No match; return as-is + return url + + +@router.post("/load/url", response_model=Optional[dict]) +async def load_function_from_url( + request: Request, form_data: LoadUrlForm, user=Depends(get_admin_user) +): + url = str(form_data.url) + if not url: + raise HTTPException(status_code=400, detail="Please enter a valid URL") + + url = github_url_to_raw_url(url) + url_parts = url.rstrip("/").split("/") + + file_name = url_parts[-1] + function_name = ( + file_name[:-3] + if ( + file_name.endswith(".py") + and (not file_name.startswith(("main.py", "index.py", "__init__.py"))) + ) + else url_parts[-2] if len(url_parts) > 1 else "function" + ) + + try: + async with aiohttp.ClientSession() as session: + async with session.get( + url, headers={"Content-Type": "application/json"} + ) as resp: + if resp.status != 200: + raise HTTPException( + status_code=resp.status, detail="Failed to fetch the function" + ) + data = await resp.text() + if not data: + raise HTTPException( + status_code=400, detail="No data received from the URL" + ) + return { + "name": function_name, + "content": data, + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error importing function: {e}") + + ############################ # SyncFunctions ############################ diff --git a/src/lib/apis/functions/index.ts b/src/lib/apis/functions/index.ts index f1a9bf5a0..60e88118b 100644 --- a/src/lib/apis/functions/index.ts +++ b/src/lib/apis/functions/index.ts @@ -62,6 +62,40 @@ export const getFunctions = async (token: string = '') => { return res; }; +export const loadFunctionByUrl = async (token: string = '', url: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/load/url`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + url + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const exportFunctions = async (token: string = '') => { let error = null; diff --git a/src/lib/components/admin/Functions.svelte b/src/lib/components/admin/Functions.svelte index b4f78fdfc..afd78303f 100644 --- a/src/lib/components/admin/Functions.svelte +++ b/src/lib/components/admin/Functions.svelte @@ -34,6 +34,7 @@ import ChevronRight from '../icons/ChevronRight.svelte'; import XMark from '../icons/XMark.svelte'; import AddFunctionMenu from './Functions/AddFunctionMenu.svelte'; + import ImportModal from './Functions/ImportModal.svelte'; const i18n = getContext('i18n'); @@ -200,6 +201,16 @@ + { + sessionStorage.function = JSON.stringify({ + ...func + }); + goto('/admin/functions/create'); + }} +/> +
@@ -239,7 +250,7 @@ createHandler={() => { goto('/admin/functions/create'); }} - importFromGithubHandler={() => { + importFromLinkHandler={() => { showImportModal = true; }} > diff --git a/src/lib/components/admin/Functions/AddFunctionMenu.svelte b/src/lib/components/admin/Functions/AddFunctionMenu.svelte index a27400081..6c0f59e1f 100644 --- a/src/lib/components/admin/Functions/AddFunctionMenu.svelte +++ b/src/lib/components/admin/Functions/AddFunctionMenu.svelte @@ -15,11 +15,12 @@ import Plus from '$lib/components/icons/Plus.svelte'; import Pencil from '$lib/components/icons/Pencil.svelte'; import PencilSolid from '$lib/components/icons/PencilSolid.svelte'; + import Link from '$lib/components/icons/Link.svelte'; const i18n = getContext('i18n'); export let createHandler: Function; - export let importFromGithubHandler: Function; + export let importFromLinkHandler: Function; export let onClose: Function = () => {}; @@ -62,14 +63,14 @@
diff --git a/src/lib/components/admin/Functions/ImportModal.svelte b/src/lib/components/admin/Functions/ImportModal.svelte index e69de29bb..b19380fdf 100644 --- a/src/lib/components/admin/Functions/ImportModal.svelte +++ b/src/lib/components/admin/Functions/ImportModal.svelte @@ -0,0 +1,144 @@ + + + +
+
+
{$i18n.t('Import')}
+ +
+ +
+
+
{ + submitHandler(); + }} + > +
+
+
{$i18n.t('URL')}
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+