From 92e64c1d59cdd99b5f33a108186a1d368daab964 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 16 Oct 2026 14:31:39 +0400 Subject: [PATCH] refac --- backend/open_webui/config.py | 10 ++++++ backend/open_webui/routers/auths.py | 10 +++++- backend/open_webui/tools/builtin.py | 6 ++-- backend/open_webui/utils/middleware.py | 2 +- backend/open_webui/utils/skill_files.py | 21 +++++++++--- backend/open_webui/utils/terminals.py | 4 +-- .../components/admin/Settings/General.svelte | 32 +++++++++++++++++++ src/lib/i18n/locales/en-US/translation.json | 4 +++ 8 files changed, 77 insertions(+), 12 deletions(-) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index c91a5f7e6d..832f1b1ce7 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -2058,6 +2058,14 @@ ENABLE_FOLDERS = os.getenv('ENABLE_FOLDERS', 'True').lower() == 'true' FOLDER_MAX_FILE_COUNT = os.getenv('FOLDER_MAX_FILE_COUNT', '') +SKILL_CONTENT_MAX_CHARS = int(os.environ['SKILL_CONTENT_MAX_CHARS']) if os.getenv('SKILL_CONTENT_MAX_CHARS') else None +if SKILL_CONTENT_MAX_CHARS is not None and SKILL_CONTENT_MAX_CHARS <= 0: + raise ValueError('SKILL_CONTENT_MAX_CHARS must be a positive integer or unset') + +SKILL_PREVIEW_MAX_CHARS = int(os.environ['SKILL_PREVIEW_MAX_CHARS']) if os.getenv('SKILL_PREVIEW_MAX_CHARS') else None +if SKILL_PREVIEW_MAX_CHARS is not None and SKILL_PREVIEW_MAX_CHARS <= 0: + raise ValueError('SKILL_PREVIEW_MAX_CHARS must be a positive integer or unset') + ENABLE_CHANNELS = os.getenv('ENABLE_CHANNELS', 'False').lower() == 'true' ENABLE_ENCRYPTED_CHANNELS = os.getenv('ENABLE_ENCRYPTED_CHANNELS', 'False').lower() == 'true' @@ -2892,6 +2900,8 @@ ENABLE_LDAP_GROUP_CREATION = os.getenv('ENABLE_LDAP_GROUP_CREATION', 'False').lo LDAP_ATTRIBUTE_FOR_GROUPS = os.getenv('LDAP_ATTRIBUTE_FOR_GROUPS', 'memberOf') DEFAULT_CONFIG = { + 'skills.content_max_chars': SKILL_CONTENT_MAX_CHARS, + 'skills.preview_max_chars': SKILL_PREVIEW_MAX_CHARS, 'direct.enable': ENABLE_DIRECT_CONNECTIONS, 'direct.integrations.enable': ENABLE_DIRECT_INTEGRATIONS, 'ollama.enable': ENABLE_OLLAMA_API, diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index b48d6b1c45..701acfb360 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -82,7 +82,7 @@ from open_webui.utils.json_codec import JSONCodec from open_webui.utils.mfa import MFA_CONFIG_KEYS, MfaConfigForm, get_mfa_config, limit_account, update_mfa_config from open_webui.utils.misc import parse_duration, validate_email_format from open_webui.utils.rate_limit import RateLimiter -from pydantic import BaseModel, StrictStr, field_validator +from pydantic import BaseModel, Field, StrictStr, field_validator from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession @@ -134,6 +134,8 @@ ADMIN_CONFIG_KEYS = { 'ENABLE_CALENDAR': 'calendar.enable', 'ENABLE_MEMORIES': 'memories.enable', 'ENABLE_MEMORY_SYSTEM_CONTEXT': 'memories.system_context.enable', + 'SKILL_CONTENT_MAX_CHARS': 'skills.content_max_chars', + 'SKILL_PREVIEW_MAX_CHARS': 'skills.preview_max_chars', 'ENABLE_NOTES': 'notes.enable', 'ENABLE_USER_WEBHOOKS': 'ui.enable_user_webhooks', 'ENABLE_USER_STATUS': 'users.enable_status', @@ -1185,6 +1187,8 @@ class AdminConfig(MfaConfigForm): ENABLE_CALENDAR: bool ENABLE_MEMORIES: bool ENABLE_MEMORY_SYSTEM_CONTEXT: bool + SKILL_CONTENT_MAX_CHARS: int | None = Field(default=None, gt=0, strict=True) + SKILL_PREVIEW_MAX_CHARS: int | None = Field(default=None, gt=0, strict=True) ENABLE_NOTES: bool ENABLE_USER_WEBHOOKS: bool ENABLE_USER_STATUS: bool @@ -1233,6 +1237,10 @@ async def update_admin_config(request: Request, form_data: AdminConfig, user=Dep updates.pop('apps.enable', None) if 'I18N' not in form_data.model_fields_set: updates.pop('ui.i18n', None) + if 'SKILL_CONTENT_MAX_CHARS' not in form_data.model_fields_set: + updates.pop('skills.content_max_chars', None) + if 'SKILL_PREVIEW_MAX_CHARS' not in form_data.model_fields_set: + updates.pop('skills.preview_max_chars', None) updates['ui.default_interface_settings'] = form_data.DEFAULT_INTERFACE_SETTINGS or {} updates['folders.max_file_count'] = int(form_data.FOLDER_MAX_FILE_COUNT) if form_data.FOLDER_MAX_FILE_COUNT else '' updates['automations.max_count'] = int(form_data.AUTOMATION_MAX_COUNT) if form_data.AUTOMATION_MAX_COUNT else '' diff --git a/backend/open_webui/tools/builtin.py b/backend/open_webui/tools/builtin.py index 297b3b055c..dcbdfe5201 100644 --- a/backend/open_webui/tools/builtin.py +++ b/backend/open_webui/tools/builtin.py @@ -3550,7 +3550,7 @@ async def view_skill( { 'id': skill.id, 'name': snapshot['name'], - **skill_overview(snapshot['content'], file_summaries(snapshot['data']['files'])), + **await skill_overview(snapshot['content'], file_summaries(snapshot['data']['files'])), }, ensure_ascii=False, ) @@ -3573,7 +3573,7 @@ async def read_skill_file( :param id: Skill ID. :param path: Relative path within the skill. :param offset: Character offset for paging text. - :param max_chars: Maximum characters to return, up to 20000 (default 10000). + :param max_chars: Maximum characters to return, capped by the admin skill content limit (default 10000). """ try: from types import SimpleNamespace @@ -3627,7 +3627,7 @@ async def read_skill_file( return JSONCodec.dumps( { 'path': path, - **skill_content_page(file['content'], offset, max_chars), + **await skill_content_page(file['content'], offset, max_chars), }, ensure_ascii=False, ) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 6d8cfe2b0a..7a806a7ebe 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -2531,7 +2531,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): continue skill_versions[skill.id] = version_id skill_tools_enabled = use_builtin_tools and model_builtin_tools.get('skills', True) - overview = skill_overview( + overview = await skill_overview( snapshot['content'], [f['path'] for f in snapshot['data']['files'] if f['path'] != 'SKILL.md'], tools_enabled=skill_tools_enabled, diff --git a/backend/open_webui/utils/skill_files.py b/backend/open_webui/utils/skill_files.py index 220f94fa12..47a53013df 100644 --- a/backend/open_webui/utils/skill_files.py +++ b/backend/open_webui/utils/skill_files.py @@ -31,9 +31,15 @@ SKILL_GREP_EXCERPT_MAX_CHARS = 1_200 SKILL_GREP_LOCATIONS_MAX_CHARS = 2_000 -def skill_content_page(content: str, offset: int = 0, max_chars: int = SKILL_CONTENT_MAX_CHARS) -> dict: +async def skill_content_page( + content: str, offset: int = 0, max_chars: int = SKILL_CONTENT_MAX_CHARS, *, limit: int | None = None +) -> dict: + if limit is None: + from open_webui.models.config import Config + + limit = await Config.get('skills.content_max_chars') or SKILL_CONTENT_MAX_CHARS offset = max(0, offset) - end = offset + min(SKILL_CONTENT_MAX_CHARS, max(1, max_chars)) + end = offset + min(limit, max(1, max_chars)) return {'content': content[offset:end], 'next_offset': end if end < len(content) else None} @@ -196,16 +202,21 @@ def grep_skill_content(files: list[dict], pattern: str, case_insensitive: bool = return serialized -def skill_overview(content: str, files: list, field: str = 'files', tools_enabled: bool = True) -> dict: +async def skill_overview(content: str, files: list, field: str = 'files', tools_enabled: bool = True) -> dict: if not tools_enabled: return { 'content': content[:SKILL_INJECTION_MAX_CHARS], 'next_offset': SKILL_INJECTION_MAX_CHARS if len(content) > SKILL_INJECTION_MAX_CHARS else None, **bounded_skill_manifest(files, field), } - large = len(content) > SKILL_CONTENT_MAX_CHARS + from open_webui.models.config import Config + + limits = await Config.get_many('skills.content_max_chars', 'skills.preview_max_chars') + content_limit = limits.get('skills.content_max_chars') or SKILL_CONTENT_MAX_CHARS + preview_limit = limits.get('skills.preview_max_chars') or SKILL_PREVIEW_MAX_CHARS + large = len(content) > content_limit return { - **skill_content_page(content, max_chars=SKILL_PREVIEW_MAX_CHARS if large else SKILL_CONTENT_MAX_CHARS), + **await skill_content_page(content, max_chars=preview_limit if large else content_limit, limit=content_limit), **bounded_skill_manifest(files, field, SKILL_OVERVIEW_LIST_MAX_CHARS), } diff --git a/backend/open_webui/utils/terminals.py b/backend/open_webui/utils/terminals.py index b1432c50dd..1bb2369629 100644 --- a/backend/open_webui/utils/terminals.py +++ b/backend/open_webui/utils/terminals.py @@ -287,10 +287,10 @@ async def get_terminal_skill( 'description': skill.get('description'), 'directory': directory, **( - skill_overview(skill['content'], resources, 'resources', tools_enabled) + await skill_overview(skill['content'], resources, 'resources', tools_enabled) if overview else { - **skill_content_page(skill['content'], offset, max_chars), + **await skill_content_page(skill['content'], offset, max_chars), **bounded_skill_manifest(resources, 'resources'), } ), diff --git a/src/lib/components/admin/Settings/General.svelte b/src/lib/components/admin/Settings/General.svelte index 0256410e30..db9c9ea478 100644 --- a/src/lib/components/admin/Settings/General.svelte +++ b/src/lib/components/admin/Settings/General.svelte @@ -84,6 +84,8 @@ const cleaned = entriesToI18n(uiI18nEntries); const res = await updateAdminConfig(localStorage.token, { ...adminConfig, + SKILL_CONTENT_MAX_CHARS: adminConfig.SKILL_CONTENT_MAX_CHARS ?? null, + SKILL_PREVIEW_MAX_CHARS: adminConfig.SKILL_PREVIEW_MAX_CHARS ?? null, DEFAULT_INTERFACE_SETTINGS: defaultInterfaceSettings, I18N: cleaned }); @@ -337,6 +339,36 @@ /> {/if} + + + + + +