fix: expose model/selected skills as a lazy-loaded view_skill manifest

This commit is contained in:
Classic298 2026-06-02 10:25:20 +02:00 • committed by GitHub
parent 4be25c4fdd
commit 8516c80762
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 34 additions and 53 deletions

View file

@ -2619,19 +2619,28 @@ async def process_chat_payload(request, form_data, user, metadata, model):
# Otherwise, save any tools that filter inlets added for merging later.
inlet_filter_tools = None if payload_tools else form_data.get('tools', None)
# Skills — extract IDs from message content (<$skillId|label> tags) so
# persisted chats work without relying on the frontend to send skill_ids.
user_skill_ids = set(form_data.pop('skill_ids', None) or [])
user_skill_ids |= extract_skill_ids_from_messages(form_data.get('messages', []))
# Skills are surfaced two ways:
# - $-mention tags (<$skillId|label>) in the message pull a skill's full content
# into the chat immediately (extracted here so persisted chats work too).
# - skill_ids (the integrations menu, where the frontend also pre-selects the
# model's attached skills) plus the model's own attached skills are exposed as a
# lightweight <available_skills> manifest, loaded on demand via view_skill.
mention_skill_ids = extract_skill_ids_from_messages(form_data.get('messages', []))
menu_skill_ids = set(form_data.pop('skill_ids', None) or [])
model_skill_ids = set(model.get('info', {}).get('meta', {}).get('skillIds', []))
all_skill_ids = mention_skill_ids | menu_skill_ids | model_skill_ids
# The view_skill tool is only registered for native function calling with builtin
# tools enabled (see get_builtin_tools); when it can't be, manifest skills are
# injected as full content too so they stay usable.
builtin_tools_enabled = (model.get('info', {}).get('meta', {}).get('capabilities') or {}).get('builtin_tools', True)
skills_lazy_load = metadata.get('params', {}).get('function_calling') == 'native' and builtin_tools_enabled
all_skill_ids = user_skill_ids | model_skill_ids
available_skills = []
if all_skill_ids:
from open_webui.models.skills import Skills as SkillsModel
accessible_skill_ids = {s.id for s in await SkillsModel.get_skills_by_user_id(user.id, 'read')}
available_skills = []
for sid in all_skill_ids:
if sid in accessible_skill_ids:
s = await SkillsModel.get_skill_by_id(sid)
@ -2640,15 +2649,15 @@ async def process_chat_payload(request, form_data, user, metadata, model):
skill_descriptions = ''
for skill in available_skills:
if skill.id in user_skill_ids:
# User-selected: inject full content
# $-mentioned skills (and, when view_skill can't be registered, every skill)
# get full content injected; the rest are lightweight manifest entries.
if skill.id in mention_skill_ids or not skills_lazy_load:
form_data['messages'] = add_or_update_system_message(
f'<skill name="{skill.name}">\n{skill.content}\n</skill>',
form_data['messages'],
append=True,
)
else:
# Model-attached: name+description only
skill_descriptions += f'<skill>\n<id>{skill.id}</id>\n<name>{skill.name}</name>\n<description>{skill.description or ""}</description>\n</skill>\n'
if skill_descriptions:
@ -2838,11 +2847,8 @@ async def process_chat_payload(request, form_data, user, metadata, model):
if mcp_clients:
metadata['mcp_clients'] = mcp_clients
# Inject builtin tools for native function calling based on enabled features and model capability
# Check if builtin_tools capability is enabled for this model (defaults to True if not specified)
builtin_tools_enabled = (model.get('info', {}).get('meta', {}).get('capabilities') or {}).get(
'builtin_tools', True
)
# Inject builtin tools for native function calling based on enabled features and model capability.
# builtin_tools_enabled (model "builtin_tools" capability) was resolved with the skills above.
if metadata.get('params', {}).get('function_calling') == 'native' and builtin_tools_enabled:
# Add file context to user messages
chat_id = metadata.get('chat_id')
@ -2852,7 +2858,10 @@ async def process_chat_payload(request, form_data, user, metadata, model):
{
**extra_params,
'__event_emitter__': event_emitter,
'__skill_ids__': [s.id for s in available_skills if s.id not in user_skill_ids],
# This branch only runs when skills_lazy_load is True; $-mentioned
# skills are injected as full content, the rest are manifest entries
# the model loads via view_skill.
'__skill_ids__': [s.id for s in available_skills if s.id not in mention_skill_ids],
},
features,
model,

View file

@ -374,7 +374,11 @@
selectedToolIds = selectedToolIds.filter((id) => !id.startsWith('direct_server:'));
}
// Set Default Skills
// Pre-select model-attached skills so they appear enabled in the integrations
// menu. These — and any skill the user enables there or via $-mention — are sent
// as skill_ids and exposed to the model as an <available_skills> manifest; the
// model loads full content on demand via the builtin view_skill tool rather than
// having it injected up front.
if (model?.info?.meta?.skillIds) {
selectedSkillIds = [
...new Set(
@ -2386,44 +2390,12 @@
}
}
// Parse skill mentions (<$skillId|label>) from user messages
const skillMentionRegex = /<\$([^|>]+)\|?[^>]*>/g;
// Skills enabled via the integrations menu (the model's attached skills are
// pre-selected here) are sent as skill_ids and exposed to the model as a manifest,
// loaded on demand via view_skill. $-mention skills are left inline in the message:
// the backend extracts them, injects their full content into the chat, and strips
// the markup — so a mention is pulled into the chat rather than lazy-loaded.
const skillIds = [...selectedSkillIds];
const mentionSkillIds = [];
for (const message of messages) {
const content =
typeof message.content === 'string' ? message.content : (message.content?.[0]?.text ?? '');
for (const match of content.matchAll(skillMentionRegex)) {
if (!mentionSkillIds.includes(match[1])) {
mentionSkillIds.push(match[1]);
}
if (!skillIds.includes(match[1])) {
skillIds.push(match[1]);
}
}
}
// Strip skill mentions from message content
if (mentionSkillIds.length > 0) {
messages = messages.map((message) => {
if (typeof message.content === 'string') {
return {
...message,
content: message.content.replace(/<\$[^>]+>/g, '').trim()
};
} else if (Array.isArray(message.content)) {
return {
...message,
content: message.content.map((part) =>
part.type === 'text'
? { ...part, text: part.text.replace(/<\$[^>]+>/g, '').trim() }
: part
)
};
}
return message;
});
}
// Use the user-selected terminal from the dropdown
const activeTerminalId = $selectedTerminalId ?? null;