From 0edd731c7422870aa109fd0b758c6d68c06655da Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 12 Sep 2026 17:11:37 -0400 Subject: [PATCH] refac --- backend/open_webui/utils/middleware.py | 20 ++++++++++--------- .../Markdown/MarkdownInlineTokens.svelte | 7 ++++++- .../components/common/RichTextInput.svelte | 9 ++++++++- src/lib/stores/index.ts | 2 +- src/lib/utils/marked/mention-extension.ts | 6 ++++++ 5 files changed, 32 insertions(+), 12 deletions(-) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 8ab00a9185..a07bd9f6d0 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -2240,7 +2240,7 @@ def sanitize_tool_pairs(messages: list[dict]) -> list[dict]: return sanitized -# Ids are validated as [a-z0-9_-]+ on create; matching that keeps ordinary "<$..." text intact. +# Match candidate mentions using the same ID characters allowed on skill creation. SKILL_MENTION_RE = re.compile(r'<(?:\$([a-z0-9_-]+)(?:\|[^>]*)?|/([a-z0-9_-]+)\|[^>]*)>') @@ -2263,25 +2263,27 @@ 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(r'<(?:\$([a-z0-9_-]+)(?:\|([^>]*))?|/([a-z0-9_-]+)\|([^>]*))>') -def strip_skill_mentions(messages: list[dict]) -> None: - """Replace <$skillId|label> and mention tags with the label in-place.""" +def strip_skill_mentions(messages: list[dict], skill_ids: set[str]) -> None: + """Replace mentions of resolved skills with their label, preserving all other text.""" def label(match): - return match.group(1) or match.group(2) or '' + if (match.group(1) or match.group(3)) not in skill_ids: + return match.group(0) + return match.group(2) or match.group(4) or '' for message in messages: content = message.get('content') if isinstance(content, str) and SKILL_MENTION_STRIP_RE.search(content): - message['content'] = SKILL_MENTION_STRIP_RE.sub(label, content).strip() + message['content'] = SKILL_MENTION_STRIP_RE.sub(label, content) elif isinstance(content, list): for part in content: if isinstance(part, dict) and part.get('type') == 'text': text = part.get('text', '') if SKILL_MENTION_STRIP_RE.search(text): - part['text'] = SKILL_MENTION_STRIP_RE.sub(label, text).strip() + part['text'] = SKILL_MENTION_STRIP_RE.sub(label, text) async def connect_mcp_server( @@ -2776,8 +2778,8 @@ async def process_chat_payload(request, form_data, user, metadata, model): append=True, ) - # Strip <$skillId|label> mention tags so the model doesn't see raw markup. - strip_skill_mentions(form_data.get('messages', [])) + # 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}) prompt = get_last_user_message(form_data['messages']) diff --git a/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte index f5989cb40d..e90b04d641 100644 --- a/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte +++ b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte @@ -9,6 +9,7 @@ const i18n = getContext('i18n'); import { WEBUI_BASE_URL } from '$lib/constants'; + import { skills } from '$lib/stores'; import { copyToClipboard, safeLinkUrl, unescapeHtml } from '$lib/utils'; import Image from '$lib/components/common/Image.svelte'; @@ -128,7 +129,11 @@ }} > {:else if token.type === 'mention'} - + {#if token.triggerChar === '$' && !$skills?.some((skill) => skill.id === token.id && skill.is_active)} + {token.raw} + {:else} + + {/if} {:else if token.type === 'footnote'} {@html DOMPurify.sanitize( `${token.escapedText}` diff --git a/src/lib/components/common/RichTextInput.svelte b/src/lib/components/common/RichTextInput.svelte index d8f47dad9c..979fab49ba 100644 --- a/src/lib/components/common/RichTextInput.svelte +++ b/src/lib/components/common/RichTextInput.svelte @@ -2,6 +2,7 @@ import { marked } from 'marked'; import DOMPurify from 'dompurify'; import equal from 'fast-deep-equal'; + import { skills } from '$lib/stores'; marked.use({ breaks: true, @@ -522,9 +523,15 @@ // Now replace the escaped mention patterns back into real spans const withMentions = escaped.replace( /<([@#$])([^|&\s]+)(?:\|([^&]*?))?>|<\/([\w.\-:/]+)\|([^&]*?)>/g, - (_, ch, id, label, slashSkillId, slashSkillLabel) => { + (match, ch, id, label, slashSkillId, slashSkillLabel) => { const mentionChar = ch || '$'; const mentionId = id || slashSkillId; + if ( + mentionChar === '$' && + !$skills?.some((skill) => skill.id === mentionId && skill.is_active) + ) { + return match; + } const display = (label || slashSkillLabel)?.length ? label || slashSkillLabel : mentionId; diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 270451dbf6..98a63429c0 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -69,7 +69,7 @@ export const models: Writable = writable([]); export const knowledge: Writable = writable(null); export const tools = writable(null); -export const skills = writable(null); +export const skills: Writable = writable(null); export const functions = writable(null); export type WorkspaceSection = 'models' | 'knowledge' | 'prompts' | 'skills' | 'tools'; diff --git a/src/lib/utils/marked/mention-extension.ts b/src/lib/utils/marked/mention-extension.ts index 2932da4c7c..efbb74fdf0 100644 --- a/src/lib/utils/marked/mention-extension.ts +++ b/src/lib/utils/marked/mention-extension.ts @@ -1,4 +1,7 @@ // mention-extension.ts +import { get } from 'svelte/store'; +import { skills } from '$lib/stores'; + type MentionOptions = { triggerChar?: string; // default "@" className?: string; // default "mention" @@ -20,6 +23,9 @@ 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)) { + return escapeHtml(token.raw); + } const cls = options.className ?? 'mention'; const extra = options.extraAttrs ?? {};