mirror of
https://github.com/open-webui/open-webui.git
synced 2026-09-16 23:43:03 +00:00
refac
This commit is contained in:
parent
55b7343be8
commit
e69236bccb
15 changed files with 386 additions and 40 deletions
|
|
@ -10,6 +10,7 @@ import asyncio
|
|||
import logging
|
||||
import time
|
||||
from typing import Literal, Optional
|
||||
from urllib.parse import unquote
|
||||
|
||||
from fastapi import HTTPException, Request
|
||||
|
||||
|
|
@ -3468,6 +3469,7 @@ async def view_skill(
|
|||
id: str,
|
||||
__request__: Request = None,
|
||||
__user__: dict = None,
|
||||
__metadata__: dict = None,
|
||||
) -> str:
|
||||
"""
|
||||
Load the full instructions of a skill by its id from the available skills manifest.
|
||||
|
|
@ -3483,6 +3485,16 @@ async def view_skill(
|
|||
return JSONCodec.dumps({'error': 'User context not available'})
|
||||
|
||||
try:
|
||||
terminal_skill_prefix = 'terminal:'
|
||||
if isinstance(id, str) and id.startswith(terminal_skill_prefix):
|
||||
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)
|
||||
if not skill:
|
||||
return JSONCodec.dumps({'error': f"Skill '{id}' not found"})
|
||||
return JSONCodec.dumps(skill, ensure_ascii=False)
|
||||
|
||||
from open_webui.models.access_grants import AccessGrants
|
||||
from open_webui.models.skills import Skills
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import textwrap
|
|||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Any, Optional
|
||||
from urllib.parse import unquote
|
||||
from uuid import uuid4
|
||||
|
||||
from aiocache import cached
|
||||
|
|
@ -2293,8 +2294,9 @@ def sanitize_tool_pairs(messages: list[dict]) -> list[dict]:
|
|||
return sanitized
|
||||
|
||||
|
||||
# Match candidate mentions using the same ID characters allowed on skill creation.
|
||||
SKILL_MENTION_RE = re.compile(r'<(?:\$([a-z0-9_-]+)(?:\|[^>]*)?|/([a-z0-9_-]+)\|[^>]*)>')
|
||||
# Match DB skill IDs and terminal skill IDs created by the $ picker.
|
||||
SKILL_ID_RE = r'(?:[a-z0-9_-]+|terminal:[^|>\s]+)'
|
||||
SKILL_MENTION_RE = re.compile(rf'<(?:\$({SKILL_ID_RE})(?:\|[^>]*)?|/({SKILL_ID_RE})\|[^>]*)>')
|
||||
|
||||
|
||||
def _get_text_parts(message: dict) -> list[str]:
|
||||
|
|
@ -2316,7 +2318,7 @@ def extract_skill_ids_from_messages(messages: list[dict]) -> set[str]:
|
|||
return ids
|
||||
|
||||
|
||||
SKILL_MENTION_STRIP_RE = re.compile(r'<(?:\$([a-z0-9_-]+)(?:\|([^>]*))?|/([a-z0-9_-]+)\|([^>]*))>')
|
||||
SKILL_MENTION_STRIP_RE = re.compile(rf'<(?:\$({SKILL_ID_RE})(?:\|([^>]*))?|/({SKILL_ID_RE})\|([^>]*))>')
|
||||
|
||||
|
||||
def strip_skill_mentions(messages: list[dict], skill_ids: set[str]) -> None:
|
||||
|
|
@ -2751,6 +2753,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
terminal_id = form_data.pop('terminal_id', None)
|
||||
files = form_data.pop('files', None)
|
||||
form_data.pop('folder_id', None)
|
||||
metadata['terminal_id'] = terminal_id
|
||||
|
||||
# If the original caller provided tools, use them as-is (skip resolution).
|
||||
# Otherwise, save any tools that filter inlets added for merging later.
|
||||
|
|
@ -2764,6 +2767,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
| mentioned_skill_ids
|
||||
)
|
||||
available_skills = []
|
||||
terminal_skills = []
|
||||
view_skill_ids = []
|
||||
chat = None
|
||||
if is_saved_chat_id(metadata.get('chat_id')):
|
||||
|
|
@ -2800,14 +2804,27 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
and (model.get('info', {}).get('meta', {}).get('capabilities') or {}).get('builtin_tools', True)
|
||||
)
|
||||
|
||||
if skill_ids:
|
||||
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.skills import format_terminal_skill_context, format_terminal_skill_manifest_entry
|
||||
from open_webui.utils.terminals import get_terminal_request_info, get_terminal_skill
|
||||
|
||||
accessible_skills = {s.id: s for s in await SkillsModel.get_skills(user_id=user.id, ids=skill_ids)}
|
||||
for sid in skill_ids:
|
||||
s = accessible_skills.get(sid)
|
||||
if s and s.is_active:
|
||||
available_skills.append(s)
|
||||
terminal_skill_prefix = 'terminal:'
|
||||
db_skill_ids = [sid for sid in skill_ids if not sid.startswith(terminal_skill_prefix)]
|
||||
terminal_skill_ids = [sid for sid in skill_ids if sid.startswith(terminal_skill_prefix)]
|
||||
|
||||
if use_builtin_tools:
|
||||
accessible_skills = {s.id: s for s in await SkillsModel.get_skills(user_id=user.id)}
|
||||
db_skill_ids = sorted(accessible_skills)
|
||||
else:
|
||||
accessible_skills = {s.id: s for s in await SkillsModel.get_skills(user_id=user.id, ids=db_skill_ids)}
|
||||
|
||||
for sid in db_skill_ids:
|
||||
skill = accessible_skills.get(sid)
|
||||
if skill and skill.is_active:
|
||||
available_skills.append(skill)
|
||||
|
||||
skill_manifest = ''
|
||||
for skill in available_skills:
|
||||
|
|
@ -2824,6 +2841,46 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
f'<description>{skill.description or ""}</description>\n</skill>\n'
|
||||
)
|
||||
|
||||
terminal_request = (
|
||||
await get_terminal_request_info(request, user, metadata, 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 []
|
||||
|
||||
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)
|
||||
|
||||
if skill_manifest:
|
||||
form_data['messages'] = add_or_update_system_message(
|
||||
f'<available_skills>\n{skill_manifest}</available_skills>',
|
||||
|
|
@ -2832,7 +2889,8 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
)
|
||||
|
||||
# Strip only resolved skill mentions; ordinary text such as Perl's <$fh> stays intact.
|
||||
strip_skill_mentions(form_data.get('messages', []), {s.id for s in available_skills})
|
||||
resolved_skill_ids = {s.id for s in available_skills} | {s['id'] for s in terminal_skills}
|
||||
strip_skill_mentions(form_data.get('messages', []), resolved_skill_ids)
|
||||
|
||||
prompt = get_last_user_message(form_data['messages'])
|
||||
|
||||
|
|
@ -2841,7 +2899,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
# the stripped result is an empty string which causes 400 errors on providers
|
||||
# that reject empty content blocks (e.g. AWS Bedrock ConverseStream).
|
||||
if not prompt or not prompt.strip():
|
||||
fallback = ', '.join(s.name for s in available_skills)
|
||||
fallback = ', '.join([s.name for s in available_skills] + [s['name'] for s in terminal_skills])
|
||||
if fallback:
|
||||
set_last_user_message_content(fallback, form_data['messages'])
|
||||
prompt = fallback
|
||||
|
|
@ -2959,7 +3017,10 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
# Resolve terminal tools if terminal_id is set (outside tool_ids check
|
||||
# so system terminals work even when no other tools are selected)
|
||||
terminal_capability = (model.get('info', {}).get('meta', {}).get('capabilities') or {}).get('terminal', True)
|
||||
if terminal_id and terminal_capability:
|
||||
terminal_connection_ids = {
|
||||
connection.get('id') for connection in await Config.get('terminal_server.connections', []) or []
|
||||
}
|
||||
if terminal_id and terminal_capability and terminal_id in terminal_connection_ids:
|
||||
try:
|
||||
terminal_result = await get_terminal_tools(
|
||||
request,
|
||||
|
|
|
|||
26
backend/open_webui/utils/skills.py
Normal file
26
backend/open_webui/utils/skills.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
"""Shared helpers for model-facing skill prompts."""
|
||||
|
||||
|
||||
def format_terminal_skill_context(skill: dict) -> str:
|
||||
resources = skill.get('resources') if isinstance(skill.get('resources'), list) else []
|
||||
parts = [f'<skill name="{skill.get("name") or ""}">', skill.get('content') or '']
|
||||
if skill.get('directory'):
|
||||
parts.append(f'<directory>{skill["directory"]}</directory>')
|
||||
if resources:
|
||||
parts.append('<resources>')
|
||||
parts.extend(f'<file>{resource}</file>' for resource in resources)
|
||||
parts.append('</resources>')
|
||||
parts.append('</skill>')
|
||||
return '\n'.join(parts)
|
||||
|
||||
|
||||
def format_terminal_skill_manifest_entry(skill: dict) -> str:
|
||||
location = skill.get('location') or skill.get('path') or ''
|
||||
location_tag = f'<location>{location}</location>\n' if location else ''
|
||||
return (
|
||||
f'<skill>\n<id>{skill["id"]}</id>\n<name>{skill["name"]}</name>\n'
|
||||
f'<description>{skill.get("description") or ""}</description>\n'
|
||||
f'<source>terminal</source>\n'
|
||||
f'{location_tag}'
|
||||
f'</skill>\n'
|
||||
)
|
||||
|
|
@ -111,3 +111,98 @@ def terminal_chat_uploads(connection: dict) -> str:
|
|||
"""Return normalized main-chat upload behavior for this connection."""
|
||||
value = (connection.get('config') or {}).get('chat_uploads')
|
||||
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):
|
||||
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 {}
|
||||
terminal_id = metadata.get('terminal_id')
|
||||
if not terminal_id:
|
||||
return None
|
||||
|
||||
user_model = user if isinstance(user, UserModel) else UserModel(**user)
|
||||
connections = await Config.get('terminal_server.connections', []) or []
|
||||
connection = next((item for item in connections if item.get('id') == terminal_id), None)
|
||||
|
||||
if connection:
|
||||
if not connection.get('enabled', True):
|
||||
return None
|
||||
user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user_model.id)}
|
||||
if not await has_connection_access(user_model, connection, user_group_ids):
|
||||
return None
|
||||
|
||||
headers, cookies = await build_tool_server_headers(
|
||||
connection,
|
||||
request,
|
||||
user_model,
|
||||
metadata=metadata,
|
||||
extra_params=extra_params,
|
||||
)
|
||||
headers['Accept'] = 'application/json'
|
||||
headers['X-User-Id'] = user_model.id
|
||||
if metadata.get('chat_id'):
|
||||
headers['X-Session-Id'] = metadata['chat_id']
|
||||
terminal_context = 'automation' if metadata.get('automation_id') else 'chat'
|
||||
context_id = terminal_context_id(connection, metadata, terminal_context)
|
||||
if context_id:
|
||||
headers[TERMINAL_CONTEXT_HEADER] = context_id
|
||||
return get_terminal_server_url(connection), headers, cookies
|
||||
|
||||
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, {}
|
||||
|
||||
|
||||
async def get_terminal_skill(request, user, metadata: dict, skill_name: str, extra_params: dict | None = None) -> dict | None:
|
||||
import aiohttp
|
||||
from urllib.parse import quote
|
||||
|
||||
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
|
||||
|
||||
if not isinstance(skill, dict):
|
||||
return None
|
||||
|
||||
location = skill.get('location') or skill.get('path') or ''
|
||||
directory = location.rsplit('/', 1)[0] if '/' in location else location
|
||||
resources = skill.get('resources') if isinstance(skill.get('resources'), list) else []
|
||||
return {
|
||||
'name': skill.get('name'),
|
||||
'description': skill.get('description'),
|
||||
'content': skill.get('content'),
|
||||
'directory': directory,
|
||||
'resources': resources,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,18 @@ export type TerminalCwd = {
|
|||
root?: TerminalFileRoot;
|
||||
};
|
||||
|
||||
export type TerminalSkill = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
is_active: boolean;
|
||||
source: 'terminal';
|
||||
terminal_path: string;
|
||||
terminal_scope: 'global';
|
||||
terminal_selector: string;
|
||||
terminal_name?: string;
|
||||
};
|
||||
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
export type TerminalConnection = {
|
||||
|
|
@ -123,7 +135,7 @@ const bearerHeaders = (apiKey: string): Record<string, string> => ({
|
|||
Authorization: `Bearer ${apiKey.trim()}`
|
||||
});
|
||||
|
||||
const joinTerminalPath = (base: string, child: string) => {
|
||||
export const joinTerminalPath = (base: string, child: string) => {
|
||||
if (!child) return base;
|
||||
if (child.startsWith('/') || /^[A-Za-z]:[\\/]/.test(child)) return child;
|
||||
return `${base.replace(/[\\/]+$/, '')}/${child.replace(/^[\\/]+/, '')}`;
|
||||
|
|
@ -330,6 +342,29 @@ export const readFile = async (
|
|||
return json?.content ?? null;
|
||||
};
|
||||
|
||||
export const listTerminalSkills = async (
|
||||
connection: TerminalConnection | null,
|
||||
chatId?: string | null
|
||||
): Promise<TerminalSkill[]> => {
|
||||
if (!connection) return [];
|
||||
|
||||
const skills = await terminalRequest<TerminalSkill[]>(
|
||||
connection,
|
||||
chatId ?? null,
|
||||
'/skills'
|
||||
).catch(() => []);
|
||||
|
||||
return (Array.isArray(skills) ? skills : []).map((skill) => ({
|
||||
...skill,
|
||||
is_active: true,
|
||||
source: 'terminal',
|
||||
terminal_path: skill.terminal_path ?? (skill as any).location ?? '',
|
||||
terminal_scope: skill.terminal_scope ?? (skill as any).scope ?? 'global',
|
||||
terminal_selector: connection.selector,
|
||||
terminal_name: connection.selector
|
||||
}));
|
||||
};
|
||||
|
||||
export const downloadFileBlob = async (
|
||||
baseUrl: string,
|
||||
apiKey: string,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
artifactContents,
|
||||
tools,
|
||||
skills,
|
||||
terminalSkills,
|
||||
toolServers,
|
||||
terminalServers,
|
||||
functions,
|
||||
|
|
@ -981,6 +982,13 @@
|
|||
selectedTerminalId.set(null);
|
||||
}
|
||||
|
||||
let lastTerminalSkillSelector: string | null = null;
|
||||
$: if ($selectedTerminalId !== lastTerminalSkillSelector) {
|
||||
selectedSkillIds = selectedSkillIds.filter((id) => !id.startsWith('terminal:'));
|
||||
terminalSkills.set([]);
|
||||
lastTerminalSkillSelector = $selectedTerminalId;
|
||||
}
|
||||
|
||||
let settingDefaults = false;
|
||||
const setDefaults = async () => {
|
||||
if (settingDefaults) return;
|
||||
|
|
@ -2752,11 +2760,15 @@
|
|||
const chatCompletionEventHandler = async (data, message, chatId) => {
|
||||
const { id, done, choices, content, output, sources, selected_model_id, error, usage } = data;
|
||||
|
||||
// Store raw OR-aligned output items from backend
|
||||
// Store raw OR-aligned output items from backend
|
||||
if (output) {
|
||||
message.output = output;
|
||||
message.content = getOutputText(output);
|
||||
if (data.type === 'response.output_text.delta' && navigator.vibrate && $settings?.hapticFeedback) {
|
||||
if (
|
||||
data.type === 'response.output_text.delta' &&
|
||||
navigator.vibrate &&
|
||||
$settings?.hapticFeedback
|
||||
) {
|
||||
navigator.vibrate(5);
|
||||
}
|
||||
dispatchCallOverlayAudio(message);
|
||||
|
|
@ -3565,11 +3577,7 @@
|
|||
filter_ids: selectedFilterIds.length > 0 ? selectedFilterIds : undefined,
|
||||
tool_ids: toolIds.length > 0 ? toolIds : undefined,
|
||||
skill_ids: skillIds.length > 0 ? skillIds : undefined,
|
||||
terminal_id:
|
||||
terminalEnabled &&
|
||||
($terminalServers ?? []).some((t) => t.id && t.id === $selectedTerminalId)
|
||||
? $selectedTerminalId
|
||||
: undefined,
|
||||
terminal_id: terminalEnabled && $selectedTerminalId ? $selectedTerminalId : undefined,
|
||||
tool_servers: [
|
||||
...($toolServers ?? []).filter(
|
||||
(server, idx) => toolServerIds.includes(idx) || toolServerIds.includes(server?.id)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
showCallOverlay,
|
||||
tools,
|
||||
skills,
|
||||
terminalSkills,
|
||||
toolServers,
|
||||
terminalServers,
|
||||
user as _user,
|
||||
|
|
@ -803,7 +804,9 @@
|
|||
$: showToolsButton = ($tools ?? []).length > 0 || ($toolServers ?? []).length > 0;
|
||||
|
||||
let showSkillsButton = false;
|
||||
$: showSkillsButton = ($skills ?? []).some((skill) => skill.is_active);
|
||||
$: showSkillsButton =
|
||||
($skills ?? []).some((skill) => skill.is_active) ||
|
||||
($terminalSkills ?? []).some((skill) => skill.is_active);
|
||||
|
||||
let showWebSearchButton = false;
|
||||
$: showWebSearchButton =
|
||||
|
|
|
|||
|
|
@ -2,6 +2,18 @@
|
|||
import { resolveLocalizedResource } from '$lib/utils/localizedContent';
|
||||
import { getContext, onDestroy } from 'svelte';
|
||||
import { getSkillItems } from '$lib/apis/skills';
|
||||
import {
|
||||
listTerminalSkills,
|
||||
resolveTerminalConnection,
|
||||
type TerminalSkill
|
||||
} from '$lib/apis/terminal';
|
||||
import {
|
||||
chatId,
|
||||
selectedTerminalId,
|
||||
settings,
|
||||
terminalServers,
|
||||
terminalSkills
|
||||
} from '$lib/stores';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Cube from '$lib/components/icons/Cube.svelte';
|
||||
|
||||
|
|
@ -27,10 +39,26 @@
|
|||
});
|
||||
|
||||
const getItems = async () => {
|
||||
const res = await getSkillItems(localStorage.token, query).catch(() => null);
|
||||
if (res) {
|
||||
filteredItems = res.items;
|
||||
}
|
||||
const [res, terminalItems] = await Promise.all([
|
||||
getSkillItems(localStorage.token, query).catch(() => null),
|
||||
getTerminalItems(query)
|
||||
]);
|
||||
filteredItems = [...(res?.items ?? []), ...terminalItems];
|
||||
};
|
||||
|
||||
const getTerminalItems = async (query = ''): Promise<TerminalSkill[]> => {
|
||||
const connection = resolveTerminalConnection(
|
||||
$selectedTerminalId,
|
||||
$terminalServers ?? [],
|
||||
$settings?.terminalServers ?? [],
|
||||
localStorage.token
|
||||
);
|
||||
const items = await listTerminalSkills(connection, $chatId || null).catch(() => []);
|
||||
terminalSkills.set(items);
|
||||
const q = query.trim().toLowerCase();
|
||||
return q
|
||||
? items.filter((skill) => `${skill.name} ${skill.description}`.toLowerCase().includes(q))
|
||||
: items;
|
||||
};
|
||||
|
||||
$: if (query) {
|
||||
|
|
@ -106,7 +134,7 @@
|
|||
{resolveLocalizedResource(skill, $i18n.language)}
|
||||
</div>
|
||||
<div class="ml-2 max-w-24 shrink-0 truncate text-xs text-gray-500 dark:text-gray-400">
|
||||
{skill.id}
|
||||
{skill.source === 'terminal' ? $i18n.t('Terminal') : skill.id}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,18 @@
|
|||
import { getContext, onDestroy } from 'svelte';
|
||||
import { getPrompts } from '$lib/apis/prompts';
|
||||
import { getSkillItems } from '$lib/apis/skills';
|
||||
import {
|
||||
listTerminalSkills,
|
||||
resolveTerminalConnection,
|
||||
type TerminalSkill
|
||||
} from '$lib/apis/terminal';
|
||||
import {
|
||||
chatId,
|
||||
selectedTerminalId,
|
||||
settings,
|
||||
terminalServers,
|
||||
terminalSkills
|
||||
} from '$lib/stores';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import ChatBubbleDotted from '$lib/components/icons/ChatBubbleDotted.svelte';
|
||||
import ChatBubbleDottedChecked from '$lib/components/icons/ChatBubbleDottedChecked.svelte';
|
||||
|
|
@ -84,17 +96,33 @@
|
|||
clearTimeout(searchDebounceTimer);
|
||||
});
|
||||
|
||||
const getTerminalItems = async (query = ''): Promise<TerminalSkill[]> => {
|
||||
const connection = resolveTerminalConnection(
|
||||
$selectedTerminalId,
|
||||
$terminalServers ?? [],
|
||||
$settings?.terminalServers ?? [],
|
||||
localStorage.token
|
||||
);
|
||||
const items = await listTerminalSkills(connection, $chatId || null).catch(() => []);
|
||||
terminalSkills.set(items);
|
||||
const q = query.trim().toLowerCase();
|
||||
return q
|
||||
? items.filter((skill) => `${skill.name} ${skill.description}`.toLowerCase().includes(q))
|
||||
: items;
|
||||
};
|
||||
|
||||
const getItems = async () => {
|
||||
const [promptRes, skillRes] = await Promise.all([
|
||||
const [promptRes, skillRes, terminalItems] = await Promise.all([
|
||||
getPrompts(localStorage.token).catch(() => null),
|
||||
getSkillItems(localStorage.token, query).catch(() => null)
|
||||
getSkillItems(localStorage.token, query).catch(() => null),
|
||||
getTerminalItems(query)
|
||||
]);
|
||||
|
||||
if (promptRes) {
|
||||
prompts = promptRes;
|
||||
}
|
||||
|
||||
skills = skillRes?.items ?? [];
|
||||
skills = [...(skillRes?.items ?? []), ...terminalItems];
|
||||
};
|
||||
|
||||
export const selectUp = () => {
|
||||
|
|
@ -447,7 +475,7 @@
|
|||
{resolveLocalizedResource(skill, $i18n.language)}
|
||||
</div>
|
||||
<div class="ml-2 max-w-24 shrink-0 truncate text-xs text-gray-500 dark:text-gray-400">
|
||||
{skill.id}
|
||||
{skill.source === 'terminal' ? $i18n.t('Terminal') : skill.id}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -4,11 +4,26 @@
|
|||
import { getContext, onDestroy, tick } from 'svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
import { user, tools as _tools, skills as _skills, toolServers } from '$lib/stores';
|
||||
import {
|
||||
chatId,
|
||||
selectedTerminalId,
|
||||
settings,
|
||||
terminalServers,
|
||||
terminalSkills,
|
||||
user,
|
||||
tools as _tools,
|
||||
skills as _skills,
|
||||
toolServers
|
||||
} from '$lib/stores';
|
||||
|
||||
import { deleteOAuthSession } from '$lib/apis/auths';
|
||||
import { getTools } from '$lib/apis/tools';
|
||||
import { getSkills } from '$lib/apis/skills';
|
||||
import {
|
||||
listTerminalSkills,
|
||||
resolveTerminalConnection,
|
||||
type TerminalSkill
|
||||
} from '$lib/apis/terminal';
|
||||
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
|
|
@ -174,6 +189,18 @@
|
|||
}
|
||||
};
|
||||
|
||||
const getTerminalSkillItems = async (): Promise<TerminalSkill[]> => {
|
||||
const connection = resolveTerminalConnection(
|
||||
$selectedTerminalId,
|
||||
$terminalServers ?? [],
|
||||
$settings?.terminalServers ?? [],
|
||||
localStorage.token
|
||||
);
|
||||
const items = await listTerminalSkills(connection, $chatId || null).catch(() => []);
|
||||
terminalSkills.set(items);
|
||||
return items;
|
||||
};
|
||||
|
||||
const loadTools = async (query = toolQuery) => {
|
||||
const requestId = ++toolRequestId;
|
||||
const q = query.trim();
|
||||
|
|
@ -195,7 +222,9 @@
|
|||
await _skills.set(await getSkills(localStorage.token));
|
||||
}
|
||||
if (requestId !== skillRequestId) return;
|
||||
setSkills($_skills, q);
|
||||
const terminalItems = await getTerminalSkillItems();
|
||||
if (requestId !== skillRequestId) return;
|
||||
setSkills([...($_skills ?? []), ...terminalItems], q);
|
||||
};
|
||||
|
||||
const scheduleToolSearch = () => {
|
||||
|
|
@ -252,6 +281,9 @@
|
|||
}
|
||||
};
|
||||
|
||||
const skillSourceLabel = (skill: IntegrationItem | undefined) =>
|
||||
skill?.source === 'terminal' ? $i18n.t('Terminal') : '';
|
||||
|
||||
onDestroy(() => {
|
||||
clearTimeout(toolSearchDebounceTimer);
|
||||
clearTimeout(skillSearchDebounceTimer);
|
||||
|
|
@ -680,6 +712,11 @@
|
|||
{resolveLocalizedResource(skills?.[skillId], $i18n.language, 'name')}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{#if skillSourceLabel(skills?.[skillId])}
|
||||
<div class="shrink-0 text-[0.6875rem] text-gray-500 dark:text-gray-400">
|
||||
{skillSourceLabel(skills?.[skillId])}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
const i18n = getContext('i18n');
|
||||
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { skills } from '$lib/stores';
|
||||
import { skills, terminalSkills } from '$lib/stores';
|
||||
import { copyToClipboard, safeLinkUrl, unescapeHtml } from '$lib/utils';
|
||||
|
||||
import Image from '$lib/components/common/Image.svelte';
|
||||
|
|
@ -129,7 +129,7 @@
|
|||
}}
|
||||
></iframe>
|
||||
{:else if token.type === 'mention'}
|
||||
{#if token.triggerChar === '$' && !$skills?.some((skill) => skill.id === token.id && skill.is_active)}
|
||||
{#if token.triggerChar === '$' && ![...($skills ?? []), ...($terminalSkills ?? [])].some((skill) => skill.id === token.id && skill.is_active)}
|
||||
{token.raw}
|
||||
{:else}
|
||||
<MentionToken {token} />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { resolveLocalizedResource } from '$lib/utils/localizedContent';
|
||||
import { getContext } from 'svelte';
|
||||
import { skills } from '$lib/stores';
|
||||
import { skills, terminalSkills } from '$lib/stores';
|
||||
|
||||
import Modal from '../common/Modal.svelte';
|
||||
import Collapsible from '../common/Collapsible.svelte';
|
||||
|
|
@ -12,7 +12,9 @@
|
|||
|
||||
let selectedSkills = [];
|
||||
|
||||
$: selectedSkills = ($skills ?? []).filter((skill) => selectedSkillIds.includes(skill.id));
|
||||
$: selectedSkills = [...($skills ?? []), ...($terminalSkills ?? [])].filter((skill) =>
|
||||
selectedSkillIds.includes(skill.id)
|
||||
);
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
</script>
|
||||
|
|
@ -51,6 +53,9 @@
|
|||
{resolveLocalizedResource(skill, $i18n.language, 'description')}
|
||||
</div>
|
||||
{/if}
|
||||
{#if skill.source === 'terminal'}
|
||||
<div class="text-xs text-gray-500">{$i18n.t('Terminal')}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Collapsible>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
import equal from 'fast-deep-equal';
|
||||
import { skills } from '$lib/stores';
|
||||
import { skills, terminalSkills } from '$lib/stores';
|
||||
|
||||
marked.use({
|
||||
breaks: true,
|
||||
|
|
@ -528,7 +528,9 @@
|
|||
const mentionId = id || slashSkillId;
|
||||
if (
|
||||
mentionChar === '$' &&
|
||||
!$skills?.some((skill) => skill.id === mentionId && skill.is_active)
|
||||
![...($skills ?? []), ...($terminalSkills ?? [])].some(
|
||||
(skill) => skill.id === mentionId && skill.is_active
|
||||
)
|
||||
) {
|
||||
return match;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ export const models: Writable<Model[]> = writable([]);
|
|||
export const knowledge: Writable<null | Document[]> = writable(null);
|
||||
export const tools = writable(null);
|
||||
export const skills: Writable<null | any[]> = writable(null);
|
||||
export const terminalSkills: Writable<any[]> = writable([]);
|
||||
export const functions = writable(null);
|
||||
|
||||
export type WorkspaceSection = 'models' | 'knowledge' | 'prompts' | 'skills' | 'tools';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// mention-extension.ts
|
||||
import { get } from 'svelte/store';
|
||||
import { skills } from '$lib/stores';
|
||||
import { skills, terminalSkills } from '$lib/stores';
|
||||
|
||||
type MentionOptions = {
|
||||
triggerChar?: string; // default "@"
|
||||
|
|
@ -23,7 +23,12 @@ function mentionStart(src: string) {
|
|||
|
||||
function mentionRenderer(token: any, options: MentionOptions = {}) {
|
||||
const trigger = options.triggerChar ?? '@';
|
||||
if (trigger === '$' && !get(skills)?.some((skill) => skill.id === token.id && skill.is_active)) {
|
||||
if (
|
||||
trigger === '$' &&
|
||||
![...(get(skills) ?? []), ...(get(terminalSkills) ?? [])].some(
|
||||
(skill) => skill.id === token.id && skill.is_active
|
||||
)
|
||||
) {
|
||||
return escapeHtml(token.raw);
|
||||
}
|
||||
const cls = options.className ?? 'mention';
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue