Selaa lähdekoodia

feat/enh: external tool server manual JSON spec

Timothy Jaeryang Baek 1 viikko sitten
vanhempi
commit
bad7d69a58
2 muutettua tiedostoa jossa 138 lisäystä ja 48 poistoa
  1. 37 18
      backend/open_webui/utils/tools.py
  2. 101 30
      src/lib/components/AddToolServerModal.svelte

+ 37 - 18
backend/open_webui/utils/tools.py

@@ -588,28 +588,20 @@ async def get_tool_server_data(token: str, url: str) -> Dict[str, Any]:
             error = str(err)
         raise Exception(error)
 
-    data = {
-        "openapi": res,
-        "info": res.get("info", {}),
-        "specs": convert_openapi_to_tool_payload(res),
-    }
-
-    log.info(f"Fetched data: {data}")
-    return data
+    log.debug(f"Fetched data: {res}")
+    return res
 
 
 async def get_tool_servers_data(servers: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
     # Prepare list of enabled servers along with their original index
+
+    tasks = []
     server_entries = []
     for idx, server in enumerate(servers):
         if (
             server.get("config", {}).get("enable")
             and server.get("type", "openapi") == "openapi"
         ):
-            # Path (to OpenAPI spec URL) can be either a full URL or a path to append to the base URL
-            openapi_path = server.get("path", "openapi.json")
-            full_url = get_tool_server_url(server.get("url"), openapi_path)
-
             info = server.get("info", {})
 
             auth_type = server.get("auth_type", "bearer")
@@ -625,12 +617,34 @@ async def get_tool_servers_data(servers: List[Dict[str, Any]]) -> List[Dict[str,
             if not id:
                 id = str(idx)
 
-            server_entries.append((id, idx, server, full_url, info, token))
+            server_url = server.get("url")
+            spec_type = server.get("spec_type", "url")
+
+            # Create async tasks to fetch data
+            task = None
+            if spec_type == "url":
+                # Path (to OpenAPI spec URL) can be either a full URL or a path to append to the base URL
+                openapi_path = server.get("path", "openapi.json")
+                spec_url = get_tool_server_url(server_url, openapi_path)
+                # Fetch from URL
+                task = get_tool_server_data(token, spec_url)
+            elif spec_type == "json" and server.get("spec", ""):
+                # Use provided JSON spec
+                spec_json = None
+                try:
+                    spec_json = json.loads(server.get("spec", ""))
+                except Exception as e:
+                    log.error(f"Error parsing JSON spec for tool server {id}: {e}")
+
+                if spec_json:
+                    task = asyncio.sleep(
+                        0,
+                        result=spec_json,
+                    )
 
-    # Create async tasks to fetch data
-    tasks = [
-        get_tool_server_data(token, url) for (_, _, _, url, _, token) in server_entries
-    ]
+            if task:
+                tasks.append(task)
+                server_entries.append((id, idx, server, server_url, info, token))
 
     # Execute tasks concurrently
     responses = await asyncio.gather(*tasks, return_exceptions=True)
@@ -642,8 +656,13 @@ async def get_tool_servers_data(servers: List[Dict[str, Any]]) -> List[Dict[str,
             log.error(f"Failed to connect to {url} OpenAPI tool server")
             continue
 
-        openapi_data = response.get("openapi", {})
+        response = {
+            "openapi": response,
+            "info": response.get("info", {}),
+            "specs": convert_openapi_to_tool_payload(response),
+        }
 
+        openapi_data = response.get("openapi", {})
         if info and isinstance(openapi_data, dict):
             openapi_data["info"] = openapi_data.get("info", {})
 

+ 101 - 30
src/lib/components/AddToolServerModal.svelte

@@ -27,10 +27,13 @@
 	export let direct = false;
 	export let connection = null;
 
+	let type = 'openapi'; // 'openapi', 'mcp'
+
 	let url = '';
-	let path = 'openapi.json';
 
-	let type = 'openapi'; // 'openapi', 'mcp'
+	let spec_type = 'url'; // 'url', 'json'
+	let spec = ''; // used when spec_type is 'json'
+	let path = 'openapi.json';
 
 	let auth_type = 'bearer';
 	let key = '';
@@ -149,10 +152,26 @@
 			return;
 		}
 
+		// validate spec
+		if (spec_type === 'json') {
+			try {
+				const specJSON = JSON.parse(spec);
+				spec = JSON.stringify(specJSON, null, 2);
+			} catch (e) {
+				toast.error($i18n.t('Please enter a valid JSON spec'));
+				loading = false;
+				return;
+			}
+		}
+
 		const connection = {
+			type,
 			url,
+
+			spec_type,
+			spec,
 			path,
-			type,
+
 			auth_type,
 			key,
 			config: {
@@ -173,9 +192,12 @@
 		show = false;
 
 		// reset form
+		type = 'openapi';
 		url = '';
+
+		spec_type = 'url';
+		spec = '';
 		path = 'openapi.json';
-		type = 'openapi';
 
 		key = '';
 		auth_type = 'bearer';
@@ -191,10 +213,13 @@
 
 	const init = () => {
 		if (connection) {
+			type = connection?.type ?? 'openapi';
 			url = connection.url;
+
+			spec_type = connection?.spec_type ?? 'url';
+			spec = connection?.spec ?? '';
 			path = connection?.path ?? 'openapi.json';
 
-			type = connection?.type ?? 'openapi';
 			auth_type = connection?.auth_type ?? 'bearer';
 			key = connection?.key ?? '';
 
@@ -326,35 +351,81 @@
 										<Switch bind:state={enable} />
 									</Tooltip>
 								</div>
-
-								{#if ['', 'openapi'].includes(type)}
-									<div class="flex-1 flex items-center">
-										<label for="url-or-path" class="sr-only"
-											>{$i18n.t('openapi.json URL or Path')}</label
-										>
-										<input
-											class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
-											type="text"
-											id="url-or-path"
-											bind:value={path}
-											placeholder={$i18n.t('openapi.json URL or Path')}
-											autocomplete="off"
-											required
-										/>
-									</div>
-								{/if}
 							</div>
 						</div>
 
 						{#if ['', 'openapi'].includes(type)}
-							<div
-								class={`text-xs mt-1 ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
-							>
-								{$i18n.t(`WebUI will make requests to "{{url}}"`, {
-									url: path.includes('://')
-										? path
-										: `${url}${path.startsWith('/') ? '' : '/'}${path}`
-								})}
+							<div class="flex gap-2 mt-2">
+								<div class="flex flex-col w-full">
+									<div class="flex justify-between items-center mb-0.5">
+										<div class="flex gap-2 items-center">
+											<div
+												for="select-bearer-or-session"
+												class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
+											>
+												{$i18n.t('OpenAPI Spec')}
+											</div>
+										</div>
+									</div>
+
+									<div class="flex gap-2">
+										<div class="flex-shrink-0 self-start">
+											<select
+												id="select-bearer-or-session"
+												class={`w-full text-sm bg-transparent pr-5 ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
+												bind:value={spec_type}
+											>
+												<option value="url">{$i18n.t('URL')}</option>
+												<option value="json">{$i18n.t('JSON')}</option>
+											</select>
+										</div>
+
+										<div class="flex flex-1 items-center">
+											{#if spec_type === 'url'}
+												<div class="flex-1 flex items-center">
+													<label for="url-or-path" class="sr-only"
+														>{$i18n.t('openapi.json URL or Path')}</label
+													>
+													<input
+														class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
+														type="text"
+														id="url-or-path"
+														bind:value={path}
+														placeholder={$i18n.t('openapi.json URL or Path')}
+														autocomplete="off"
+														required
+													/>
+												</div>
+											{:else if spec_type === 'json'}
+												<div
+													class={`text-xs w-full self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
+												>
+													<label for="url-or-path" class="sr-only">{$i18n.t('JSON Spec')}</label>
+													<textarea
+														class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700 text-black dark:text-white'}`}
+														bind:value={spec}
+														placeholder={$i18n.t('JSON Spec')}
+														autocomplete="off"
+														required
+														rows="5"
+													/>
+												</div>
+											{/if}
+										</div>
+									</div>
+
+									{#if ['', 'url'].includes(spec_type)}
+										<div
+											class={`text-xs mt-1 ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
+										>
+											{$i18n.t(`WebUI will make requests to "{{url}}"`, {
+												url: path.includes('://')
+													? path
+													: `${url}${path.startsWith('/') ? '' : '/'}${path}`
+											})}
+										</div>
+									{/if}
+								</div>
 							</div>
 						{/if}