mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-11 03:38:02 +00:00
refac
Some checks failed
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_OLLAMA=true free_disk:false name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_SLIM=true free_disk:false name:slim suffix:-slim]) (push) Has been cancelled
Frontend Build / Format & Build (push) Has been cancelled
Frontend Build / Unit Tests (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args: free_disk:false name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_CUDA=true
USE_CUDA_VER=cu126
free_disk:true name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_OLLAMA=true free_disk:false name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_SLIM=true free_disk:false name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args: free_disk:false name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_CUDA=true
USE_CUDA_VER=cu126
free_disk:true name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / notify-helm-charts (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
Some checks failed
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_OLLAMA=true free_disk:false name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_SLIM=true free_disk:false name:slim suffix:-slim]) (push) Has been cancelled
Frontend Build / Format & Build (push) Has been cancelled
Frontend Build / Unit Tests (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args: free_disk:false name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_CUDA=true
USE_CUDA_VER=cu126
free_disk:true name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_OLLAMA=true free_disk:false name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_SLIM=true free_disk:false name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args: free_disk:false name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_CUDA=true
USE_CUDA_VER=cu126
free_disk:true name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / notify-helm-charts (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
This commit is contained in:
parent
056e2ac37e
commit
d3acc2706d
5 changed files with 113 additions and 20 deletions
|
|
@ -69,7 +69,7 @@ from open_webui.utils.chat_id import is_saved_chat_id
|
|||
from open_webui.utils.json_codec import JSONCodec
|
||||
from open_webui.utils.notifications import notify_target
|
||||
from open_webui.utils.sanitize import sanitize_code
|
||||
from open_webui.utils.skill_files import SkillFile, SkillFileOperation
|
||||
from open_webui.utils.skill_files import SkillFile, SkillFileOperation, bounded_skill_manifest, skill_content_page
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -3488,7 +3488,7 @@ async def view_skill(
|
|||
__metadata__: dict = None,
|
||||
__event_call__: callable = None,
|
||||
) -> str:
|
||||
"""Load the current SKILL.md and its file manifest. Supporting file reads use this same snapshot.
|
||||
"""Read skill instructions and file list. Use read_skill_file to continue from next_offset.
|
||||
|
||||
:param id: Skill ID from the available skills manifest.
|
||||
"""
|
||||
|
|
@ -3528,8 +3528,8 @@ async def view_skill(
|
|||
'id': skill.id,
|
||||
'version_id': version_id,
|
||||
'name': snapshot['name'],
|
||||
'content': snapshot['content'],
|
||||
'files': file_summaries(snapshot['data']['files']),
|
||||
**skill_content_page(snapshot['content']),
|
||||
**bounded_skill_manifest(file_summaries(snapshot['data']['files'])),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
|
@ -3545,8 +3545,9 @@ async def read_skill_file(
|
|||
__request__: Request = None,
|
||||
__user__: dict = None,
|
||||
__metadata__: dict = None,
|
||||
__event_call__: callable = None,
|
||||
) -> str:
|
||||
"""Read one skill resource from the snapshot loaded by view_skill, or the current snapshot if none was loaded.
|
||||
"""Read a skill file from the loaded snapshot. Terminal skills support SKILL.md only.
|
||||
|
||||
:param id: Skill ID.
|
||||
:param path: Relative path within the skill.
|
||||
|
|
@ -3562,6 +3563,26 @@ async def read_skill_file(
|
|||
|
||||
if not __user__ or __request__ is None:
|
||||
raise ValueError('Request and user context required')
|
||||
if id.startswith('terminal:'):
|
||||
from open_webui.utils.terminals import get_terminal_skill
|
||||
|
||||
if path != 'SKILL.md':
|
||||
raise ValueError('Terminal skills support SKILL.md only; use terminal tools for supporting files.')
|
||||
skill = await get_terminal_skill(
|
||||
__request__,
|
||||
__user__,
|
||||
__metadata__ if __metadata__ is not None else {},
|
||||
unquote(id.removeprefix('terminal:')),
|
||||
{'__event_call__': __event_call__},
|
||||
offset=offset,
|
||||
max_chars=max_chars,
|
||||
refresh=False,
|
||||
)
|
||||
if not skill:
|
||||
raise ValueError(f"Skill '{id}' not found")
|
||||
return JSONCodec.dumps(
|
||||
{'path': path, 'content': skill['content'], 'next_offset': skill['next_offset']}, ensure_ascii=False
|
||||
)
|
||||
skill = await authorized_skill(id, SimpleNamespace(**__user__))
|
||||
if not skill.is_active:
|
||||
await authorized_skill(id, SimpleNamespace(**__user__), 'write')
|
||||
|
|
@ -3583,13 +3604,11 @@ async def read_skill_file(
|
|||
'url': '/workspace/skills/edit?' + urlencode({'id': id, 'version_id': version_id, 'path': path}),
|
||||
}
|
||||
)
|
||||
offset, max_chars = max(0, offset), min(100000, max(1, max_chars))
|
||||
return JSONCodec.dumps(
|
||||
{
|
||||
'path': path,
|
||||
'version_id': version_id,
|
||||
'content': file['content'][offset : offset + max_chars],
|
||||
'next_offset': offset + max_chars if offset + max_chars < len(file['content']) else None,
|
||||
**skill_content_page(file['content'], offset, max_chars),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2782,7 +2782,7 @@ 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 is not None else form_data.get('tools', None)
|
||||
|
||||
# Mentioned skills get full content; selected/default skills can be loaded through view_skill.
|
||||
# Mentioned skills get bounded content; selected/default skills can be loaded through view_skill.
|
||||
chat_context = metadata.get('chat_context') or {}
|
||||
metadata['chat_context'] = chat_context
|
||||
skill_versions = chat_context.setdefault('skill_versions', {})
|
||||
|
|
@ -2824,6 +2824,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
|
||||
if skill_ids or (use_builtin_tools and model_builtin_tools.get('skills', True)):
|
||||
from open_webui.models.skills import Skills as SkillsModel
|
||||
from open_webui.utils.skill_files import bounded_skill_manifest, format_skill_content, skill_content_page
|
||||
from open_webui.utils.terminals import (
|
||||
format_terminal_skill_context,
|
||||
format_terminal_skill_manifest_entry,
|
||||
|
|
@ -2859,8 +2860,14 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
raise
|
||||
continue
|
||||
skill_versions[skill.id] = version_id
|
||||
root_content = snapshot['content']
|
||||
resources = '\n'.join(f['path'] for f in snapshot['data']['files'] if f['path'] != 'SKILL.md')
|
||||
skill_tools_enabled = use_builtin_tools and model_builtin_tools.get('skills', True)
|
||||
root_content = format_skill_content(
|
||||
skill_content_page(snapshot['content']), skill.id, skill_tools_enabled
|
||||
)
|
||||
manifest = bounded_skill_manifest(
|
||||
[f['path'] for f in snapshot['data']['files'] if f['path'] != 'SKILL.md']
|
||||
)
|
||||
resources = '\n'.join(manifest['files'])
|
||||
resource_hint = (
|
||||
f'\nRead supporting files with read_skill_file(id="{skill.id}", path=...).\n{resources}'
|
||||
if use_builtin_tools and model_builtin_tools.get('skills', True)
|
||||
|
|
@ -2868,8 +2875,11 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
if resources
|
||||
else ''
|
||||
)
|
||||
if manifest.get('notice'):
|
||||
resource_hint += '\n' + manifest['notice']
|
||||
form_data['messages'] = add_or_update_system_message(
|
||||
f'<skill id="{skill.id}" version_id="{version_id}" name="{skill.name}">\n{root_content}{resource_hint}\n</skill>',
|
||||
f'<skill id="{skill.id}" version_id="{version_id}" name="{skill.name}">\n'
|
||||
f'{root_content}{resource_hint}\n</skill>',
|
||||
form_data['messages'],
|
||||
append=True,
|
||||
)
|
||||
|
|
@ -2901,7 +2911,9 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
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),
|
||||
format_terminal_skill_context(
|
||||
loaded, sid, use_builtin_tools and model_builtin_tools.get('skills', True)
|
||||
),
|
||||
form_data['messages'],
|
||||
append=True,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,41 @@ MAX_FILE_BYTES = 10 * 1024 * 1024
|
|||
MAX_SKILL_BYTES = 50 * 1024 * 1024
|
||||
MAX_IMPORT_BYTES = 200 * 1024 * 1024
|
||||
MAX_FILES = 1000
|
||||
SKILL_CONTENT_MAX_CHARS = 100_000
|
||||
SKILL_MANIFEST_MAX_ENTRIES = 50
|
||||
SKILL_MANIFEST_MAX_CHARS = 5_000
|
||||
|
||||
|
||||
def skill_content_page(content: str, offset: int = 0, max_chars: int = SKILL_CONTENT_MAX_CHARS) -> dict:
|
||||
offset = max(0, offset)
|
||||
end = offset + min(SKILL_CONTENT_MAX_CHARS, max(1, max_chars))
|
||||
return {'content': content[offset:end], 'next_offset': end if end < len(content) else None}
|
||||
|
||||
|
||||
def bounded_skill_manifest(entries: list, field: str = 'files') -> dict:
|
||||
bounded, size = [], 2 # Include the JSON array brackets and separators in the budget.
|
||||
for entry in entries[:SKILL_MANIFEST_MAX_ENTRIES]:
|
||||
entry_size = len(json.dumps(entry, ensure_ascii=False)) + (2 if bounded else 0)
|
||||
if size + entry_size > SKILL_MANIFEST_MAX_CHARS:
|
||||
break
|
||||
bounded.append(entry)
|
||||
size += entry_size
|
||||
result = {field: bounded}
|
||||
if len(bounded) < len(entries):
|
||||
result['notice'] = 'Additional supporting files omitted.'
|
||||
return result
|
||||
|
||||
|
||||
def format_skill_content(page: dict, skill_id: str, tools_enabled: bool) -> str:
|
||||
content = page['content']
|
||||
if page['next_offset'] is not None:
|
||||
content += '\nSkill instructions truncated.'
|
||||
if tools_enabled:
|
||||
content += (
|
||||
f'\nContinue reading with read_skill_file(id={json.dumps(skill_id)}, '
|
||||
f'path="SKILL.md", offset={page["next_offset"]}).'
|
||||
)
|
||||
return content
|
||||
|
||||
|
||||
class SkillFile(BaseModel):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""Shared routing helpers for admin-configured terminal servers."""
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import logging
|
||||
import ntpath
|
||||
import posixpath
|
||||
|
|
@ -8,6 +9,12 @@ from urllib.parse import quote
|
|||
|
||||
from open_webui.env import ENABLE_TOOL_SERVERS
|
||||
from open_webui.utils.chat_id import is_saved_chat_id
|
||||
from open_webui.utils.skill_files import (
|
||||
SKILL_CONTENT_MAX_CHARS,
|
||||
bounded_skill_manifest,
|
||||
format_skill_content,
|
||||
skill_content_page,
|
||||
)
|
||||
|
||||
TERMINAL_CONTEXT_HEADER = 'X-Terminal-Context-Id'
|
||||
TERMINAL_CONTEXT_DEFAULT = 'default'
|
||||
|
|
@ -226,36 +233,56 @@ 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
|
||||
request,
|
||||
user,
|
||||
metadata: dict,
|
||||
skill_name: str,
|
||||
extra_params: dict | None = None,
|
||||
*,
|
||||
offset: int = 0,
|
||||
max_chars: int = SKILL_CONTENT_MAX_CHARS,
|
||||
refresh: bool = True,
|
||||
) -> dict | None:
|
||||
skill = await get_terminal_json(
|
||||
request, user, metadata, f'/skills/read?name={quote(skill_name, safe="")}', extra_params
|
||||
)
|
||||
|
||||
if not isinstance(skill, dict):
|
||||
if not isinstance(skill, dict) or not isinstance(skill.get('content'), str):
|
||||
return None
|
||||
|
||||
context = metadata.get('chat_context') or {}
|
||||
metadata['chat_context'] = context
|
||||
fingerprints = context.setdefault('terminal_skill_fingerprints', {}).setdefault(metadata['terminal_id'], {})
|
||||
skill_id = f'terminal:{quote(skill_name, safe="")}'
|
||||
fingerprint = hashlib.sha256(skill['content'].encode('utf-8')).hexdigest()
|
||||
previous = fingerprints.get(skill_id)
|
||||
if not refresh and previous is not None and previous != fingerprint:
|
||||
raise ValueError('Skill changed while reading. Call view_skill again and restart reading.')
|
||||
fingerprints[skill_id] = fingerprint
|
||||
|
||||
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'),
|
||||
**skill_content_page(skill['content'], offset, max_chars),
|
||||
'directory': directory,
|
||||
'resources': resources,
|
||||
**bounded_skill_manifest(resources, 'resources'),
|
||||
}
|
||||
|
||||
|
||||
def format_terminal_skill_context(skill: dict) -> str:
|
||||
def format_terminal_skill_context(skill: dict, skill_id: str, tools_enabled: bool) -> str:
|
||||
resources = skill.get('resources') if isinstance(skill.get('resources'), list) else []
|
||||
parts = [f'<skill name="{skill.get("name") or ""}">', skill.get('content') or '']
|
||||
parts = [f'<skill name="{skill.get("name") or ""}">', format_skill_content(skill, skill_id, tools_enabled)]
|
||||
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>')
|
||||
if skill.get('notice'):
|
||||
parts.append(skill['notice'])
|
||||
parts.append('</skill>')
|
||||
return '\n'.join(parts)
|
||||
|
||||
|
|
|
|||
|
|
@ -806,7 +806,7 @@ async def get_builtin_tools(
|
|||
]
|
||||
)
|
||||
|
||||
# Skills tools - view_skill allows model to load full skill instructions on demand
|
||||
# Skills tools - view_skill loads bounded instructions, with read_skill_file for continuation.
|
||||
if is_builtin_tool_enabled('skills'):
|
||||
builtin_functions.extend([view_skill, read_skill_file, update_skill_files])
|
||||
if user.get('role') == 'admin' or await has_permission(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue