From 7ad0ae46873511723f59c7f78bfd59f7c3982e1f Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 24 Sep 2026 12:34:04 -0400 Subject: [PATCH] refac --- backend/open_webui/tools/builtin.py | 5 +- backend/open_webui/utils/middleware.py | 63 ++++++--------- backend/open_webui/utils/terminals.py | 106 ++++++++----------------- src/routes/+layout.svelte | 16 ++++ 4 files changed, 76 insertions(+), 114 deletions(-) diff --git a/backend/open_webui/tools/builtin.py b/backend/open_webui/tools/builtin.py index d80884d1cd..c1d1b164d6 100644 --- a/backend/open_webui/tools/builtin.py +++ b/backend/open_webui/tools/builtin.py @@ -3484,6 +3484,7 @@ async def view_skill( __request__: Request = None, __user__: dict = None, __metadata__: dict = None, + __event_call__: callable = None, ) -> str: """ Load the full instructions of a skill by its id from the available skills manifest. @@ -3504,7 +3505,9 @@ async def view_skill( from open_webui.utils.terminals import get_terminal_skill skill_name = unquote(id.removeprefix(terminal_skill_prefix)) - skill = await get_terminal_skill(__request__, __user__, __metadata__ or {}, skill_name) + skill = await get_terminal_skill( + __request__, __user__, __metadata__ or {}, skill_name, {'__event_call__': __event_call__} + ) if not skill: return JSONCodec.dumps({'error': f"Skill '{id}' not found"}) return JSONCodec.dumps(skill, ensure_ascii=False) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 164ec4d3a1..de2b3c49fc 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -2812,13 +2812,11 @@ async def process_chat_payload(request, form_data, user, metadata, model): ) if skill_ids or use_builtin_tools: - import aiohttp - from open_webui.env import AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA from open_webui.models.skills import Skills as SkillsModel from open_webui.utils.terminals import ( format_terminal_skill_context, format_terminal_skill_manifest_entry, - get_terminal_request_info, + get_terminal_json, get_terminal_skill, ) @@ -2852,49 +2850,34 @@ async def process_chat_payload(request, form_data, user, metadata, model): f'{skill.description or ""}\n\n' ) - terminal_request = ( - await get_terminal_request_info(request, user, metadata, extra_params) + listed = ( + await get_terminal_json(request, user, metadata, '/skills', extra_params) if terminal_id or terminal_skill_ids else None ) - listed_terminal_skills = [] - if terminal_request: - terminal_base_url, terminal_headers, terminal_cookies = terminal_request - timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA) - async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: - async with session.get( - f'{terminal_base_url.rstrip("/")}/skills', - headers=terminal_headers, - cookies=terminal_cookies, - ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, - ) as response: - if response.status == 200: - listed = await response.json() - listed_terminal_skills = listed if isinstance(listed, list) else [] + listed_terminal_skills = listed if isinstance(listed, list) else [] - if terminal_id and use_builtin_tools: - terminal_skills = listed_terminal_skills - elif terminal_skill_ids: - terminal_skill_map = {skill['id']: skill for skill in listed_terminal_skills} - terminal_skills = [skill for sid in terminal_skill_ids if (skill := terminal_skill_map.get(sid))] + if terminal_id and use_builtin_tools: + terminal_skills = listed_terminal_skills + elif terminal_skill_ids: + terminal_skill_map = {skill['id']: skill for skill in listed_terminal_skills} + terminal_skills = [skill for sid in terminal_skill_ids if (skill := terminal_skill_map.get(sid))] - for skill in terminal_skills: - sid = skill['id'] - if sid in mentioned_skill_ids or not use_builtin_tools: - skill_name = unquote(sid.removeprefix(terminal_skill_prefix)) - loaded = await get_terminal_skill( - request, user.model_dump(), metadata, skill_name, extra_params - ) - if loaded: - form_data['messages'] = add_or_update_system_message( - format_terminal_skill_context(loaded), - form_data['messages'], - append=True, - ) - else: - view_skill_ids.append(sid) - skill_manifest += format_terminal_skill_manifest_entry(skill) + for skill in terminal_skills: + sid = skill['id'] + if sid in mentioned_skill_ids or not use_builtin_tools: + skill_name = unquote(sid.removeprefix(terminal_skill_prefix)) + loaded = await get_terminal_skill(request, user.model_dump(), metadata, skill_name, extra_params) + if loaded: + form_data['messages'] = add_or_update_system_message( + format_terminal_skill_context(loaded), + form_data['messages'], + append=True, + ) + else: + view_skill_ids.append(sid) + skill_manifest += format_terminal_skill_manifest_entry(skill) if skill_manifest: form_data['messages'] = add_or_update_system_message( diff --git a/backend/open_webui/utils/terminals.py b/backend/open_webui/utils/terminals.py index e7d34550d3..66d5f0f362 100644 --- a/backend/open_webui/utils/terminals.py +++ b/backend/open_webui/utils/terminals.py @@ -119,12 +119,15 @@ def terminal_chat_uploads(connection: dict) -> str: return value if value in TERMINAL_CHAT_UPLOAD_MODES else 'default' -async def get_terminal_request_info(request, user, metadata: dict, extra_params: dict | None = None): +async def get_terminal_json(request, user, metadata: dict, path: str, extra_params: dict | None = None): + """Read from an admin terminal on the backend or a personal terminal in its browser.""" + import aiohttp + + from open_webui.env import AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA from open_webui.models.config import Config from open_webui.models.groups import Groups from open_webui.models.users import UserModel from open_webui.utils.access_control import has_connection_access - from open_webui.utils.headers import bearer_auth_header from open_webui.utils.tools import build_tool_server_headers metadata = metadata or {} @@ -163,66 +166,39 @@ async def get_terminal_request_info(request, user, metadata: dict, extra_params: headers['X-Session-Id'] = metadata['chat_id'] if context_id: headers[TERMINAL_CONTEXT_HEADER] = context_id - return get_terminal_server_url(connection), headers, cookies + timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA) + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + async with session.get( + f'{get_terminal_server_url(connection)}{path}', + headers=headers, + cookies=cookies, + ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, + allow_redirects=False, + ) as response: + return await response.json() if response.status == 200 else None - selector = str(terminal_id).rstrip('/') - direct_terminal = next( - ( - server - for server in metadata.get('tool_servers') or [] - if str(server.get('url') or '').rstrip('/') == selector - ), - None, - ) - if not direct_terminal: - return None - - headers = {'Accept': 'application/json'} - key = str(direct_terminal.get('key') or '').strip() - if key: - headers.update(bearer_auth_header(key)) - if metadata.get('chat_id'): - headers['X-Session-Id'] = metadata['chat_id'] - return selector, headers, {} + event_caller = (extra_params or {}).get('__event_call__') + if event_caller: + result = await event_caller( + { + 'type': 'request:terminal', + 'data': {'terminal_id': terminal_id, 'path': path, 'session_id': metadata.get('session_id')}, + } + ) + return result.get('data') if isinstance(result, dict) else None + return None async def get_terminal_agents_md(request, user, metadata: dict, extra_params: dict | None = None) -> str | None: """Load the selected terminal user's home AGENTS.md afresh for this turn.""" - import aiohttp - from open_webui.env import AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL - try: async with asyncio.timeout(5): - terminal_request = await get_terminal_request_info(request, user, metadata, extra_params) - if not terminal_request: + data = await get_terminal_json(request, user, metadata, '/files/cwd', extra_params) + home = data.get('home') if isinstance(data, dict) else None + if not isinstance(home, str) or not posixpath.isabs(home): return None - base_url, headers, cookies = terminal_request - async with aiohttp.ClientSession( - headers=headers, cookies=cookies, timeout=aiohttp.ClientTimeout(total=5), trust_env=True - ) as session: - async with session.get( - f'{base_url.rstrip("/")}/files/cwd', - ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, - allow_redirects=False, - ) as response: - if response.status != 200: - log.debug('Skipping terminal AGENTS.md: home lookup returned HTTP %s', response.status) - return None - data = await response.json() - home = data.get('home') if isinstance(data, dict) else None - if not isinstance(home, str) or not posixpath.isabs(home): - return None - path = posixpath.join(home, 'AGENTS.md') - async with session.get( - f'{base_url.rstrip("/")}/files/read', - params={'path': path}, - ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, - allow_redirects=False, - ) as response: - if response.status == 404: - return None - response.raise_for_status() - data = await response.json() + path = quote(posixpath.join(home, 'AGENTS.md'), safe='') + data = await get_terminal_json(request, user, metadata, f'/files/read?path={path}', extra_params) content = data.get('content') if isinstance(data, dict) else None if not isinstance(content, str) or not content.strip(): @@ -247,25 +223,9 @@ def add_terminal_agents_md(messages: list[dict], agents_md: str) -> list[dict]: async def get_terminal_skill( request, user, metadata: dict, skill_name: str, extra_params: dict | None = None ) -> dict | None: - from urllib.parse import quote - - import aiohttp - from open_webui.env import AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA - - terminal_request = await get_terminal_request_info(request, user, metadata, extra_params) - if not terminal_request: - return None - base_url, headers, cookies = terminal_request - - timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA) - async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: - async with session.get( - f'{base_url.rstrip("/")}/skills/{quote(skill_name, safe="")}', - headers=headers, - cookies=cookies, - ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, - ) as response: - skill = await response.json() if response.status == 200 else None + skill = await get_terminal_json( + request, user, metadata, f'/skills/read?name={quote(skill_name, safe="")}', extra_params + ) if not isinstance(skill, dict): return None diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 889c00a206..1e8919a5c9 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -81,6 +81,7 @@ import { getUserSettings } from '$lib/apis/users'; import dayjs from 'dayjs'; import { getChannels } from '$lib/apis/channels'; + import { resolveTerminalConnection, terminalRequest } from '$lib/apis/terminal'; const unregisterServiceWorkers = async () => { if ('serviceWorker' in navigator) { @@ -645,6 +646,21 @@ console.log('execute:tool', data); executeTool(data, cb, event.chat_id); return; + } else if (type === 'request:terminal') { + try { + const connection = resolveTerminalConnection( + data.terminal_id, + [], + $settings?.terminalServers ?? [], + localStorage.token + ); + if (!connection) throw new Error('Terminal Not Found'); + const result = await terminalRequest(connection, event.chat_id, data.path); + cb?.({ data: result }); + } catch (error) { + cb?.({ error: `${error}` }); + } + return; } else if (type === 'request:chat:completion') { console.log(data, $socket.id); const { session_id, channel, form_data, model } = data;