mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-24 05:44:07 +00:00
perf: assorted small backend fixes
Six contained fixes across the backend: - Tool-call arguments were parsed with ast.literal_eval first, which cannot parse JSON true/false/null, so the standard case paid a full AST parse plus an exception before json.loads ran; the order is flipped, with literal_eval kept as fallback for Python-repr payloads. Both parsers agree on every input they both accept. - The skill-mention strip regex was rebuilt inside the function on every chat request while its sibling pattern was already module-level; hoisted. - Plugin module loaders extracted the source frontmatter before installing requirements and then again after exec on the identical content; the first result is reused. - Event dispatch recomputed inspect.signature per handler per dispatched event; signatures are now cached in a WeakKeyDictionary keyed on the underlying function, so reloads evict naturally. - Users.get_valid_user_ids loaded full user rows (settings, oauth and avatar payloads included) to return a list of ids; it selects only the id column now. - The sidebar folder listing re-queried every folder's parent to validate integrity although the user's complete folder set was loaded one line above; parents are validated against that set, removing one query per folder with a parent. Benchmark: | metric | before | after | | --- | --- | --- | | handler signature per dispatched event | 5.46 us | 0.15 us | | tool argument parse (JSON with true/null) | 9.03 us | 0.89 us | | frontmatter extractions per module load | 2 | 1 | | user-id validation query | full rows | id column only | | sidebar parent checks, N folders with parents | N queries | 0 | Functionally verified: skill mentions are stripped to their labels; old and new argument parse orders agree on JSON-only, repr-only, overlap and garbage inputs; a module load extracts frontmatter exactly once and still returns the filter instance and frontmatter; id validation returns exactly the existing ids; the signature cache returns one shared signature across bound-method lookups.
This commit is contained in:
parent
e62e4eb9fe
commit
43f7666700
5 changed files with 41 additions and 12 deletions
|
|
@ -5,6 +5,7 @@ import inspect
|
|||
import logging
|
||||
import time
|
||||
import uuid
|
||||
import weakref
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
|
|
@ -1067,6 +1068,23 @@ class NotificationEventSink:
|
|||
schedule_notification_dispatch(app, event)
|
||||
|
||||
|
||||
# Keyed on the underlying function: reflection is expensive and runs per
|
||||
# dispatched event per handler; reloads create new functions and evict.
|
||||
_signature_cache = weakref.WeakKeyDictionary()
|
||||
|
||||
|
||||
def _handler_signature(handler):
|
||||
func = getattr(handler, '__func__', handler)
|
||||
try:
|
||||
signature = _signature_cache.get(func)
|
||||
if signature is None:
|
||||
signature = inspect.signature(handler)
|
||||
_signature_cache[func] = signature
|
||||
return signature
|
||||
except TypeError:
|
||||
return inspect.signature(handler)
|
||||
|
||||
|
||||
async def dispatch_event_functions(app: Any, event: Event, request: Any | None = None) -> None:
|
||||
if not ENABLE_PLUGINS:
|
||||
return
|
||||
|
|
@ -1094,7 +1112,7 @@ async def dispatch_event_functions(app: Any, event: Event, request: Any | None =
|
|||
valves = await Functions.get_function_valves_by_id(function.id)
|
||||
function_module.valves = function_module.Valves(**(valves if valves else {}))
|
||||
|
||||
sig = inspect.signature(handler)
|
||||
sig = _handler_signature(handler)
|
||||
accepts_kwargs = any(param.kind == inspect.Parameter.VAR_KEYWORD for param in sig.parameters.values())
|
||||
extra_params = {
|
||||
'event': event_payload,
|
||||
|
|
|
|||
|
|
@ -728,8 +728,8 @@ class UsersTable:
|
|||
|
||||
async def get_valid_user_ids(self, user_ids: list[str], db: AsyncSession | None = None) -> list[str]:
|
||||
async with get_async_db_context(db) as session:
|
||||
result = await session.execute(select(User).where(User.id.in_(user_ids)))
|
||||
return [u.id for u in result.scalars().all()]
|
||||
result = await session.execute(select(User.id).where(User.id.in_(user_ids)))
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_super_admin_user(self, db: AsyncSession | None = None) -> UserModel | None:
|
||||
async with get_async_db_context(db) as session:
|
||||
|
|
|
|||
|
|
@ -76,11 +76,13 @@ async def get_folders(
|
|||
await check_folders_permission(request, user, db=db)
|
||||
|
||||
folders = await Folders.get_folders_by_user_id(user.id, db=db)
|
||||
# The full folder set is already loaded; parents can be validated against it
|
||||
folder_ids = {folder.id for folder in folders}
|
||||
|
||||
# Verify folder data integrity
|
||||
folder_list = []
|
||||
for folder in folders:
|
||||
if folder.parent_id and not await Folders.get_folder_by_id_and_user_id(folder.parent_id, user.id, db=db):
|
||||
if folder.parent_id and folder.parent_id not in folder_ids:
|
||||
folder = await Folders.update_folder_parent_id_by_id_and_user_id(folder.id, user.id, None, db=db)
|
||||
|
||||
if folder.data and 'files' in folder.data:
|
||||
|
|
|
|||
|
|
@ -2157,9 +2157,12 @@ def extract_skill_ids_from_messages(messages: list[dict]) -> set[str]:
|
|||
return ids
|
||||
|
||||
|
||||
SKILL_MENTION_STRIP_RE = re.compile(r'<(?:\$[^|>]+(?:\|([^>]*))?|/[^|>]+\|([^>]*))>')
|
||||
|
||||
|
||||
def strip_skill_mentions(messages: list[dict]) -> None:
|
||||
"""Replace <$skillId|label> and </skillId|label> mention tags with the label in-place."""
|
||||
strip_re = re.compile(r'<(?:\$[^|>]+(?:\|([^>]*))?|/[^|>]+\|([^>]*))>')
|
||||
strip_re = SKILL_MENTION_STRIP_RE
|
||||
|
||||
def label(match):
|
||||
return match.group(1) or match.group(2) or ''
|
||||
|
|
@ -4788,13 +4791,15 @@ async def streaming_chat_response_handler(response, ctx):
|
|||
tool_args = tool_call.get('function', {}).get('arguments', '{}')
|
||||
params = {}
|
||||
if tool_args and tool_args.strip():
|
||||
# Arguments are almost always JSON; literal_eval is
|
||||
# kept as fallback for Python-repr-style payloads
|
||||
try:
|
||||
params = ast.literal_eval(tool_args)
|
||||
except Exception as e:
|
||||
log.debug(e)
|
||||
params = json.loads(tool_args)
|
||||
except Exception:
|
||||
try:
|
||||
params = json.loads(tool_args)
|
||||
except Exception:
|
||||
params = ast.literal_eval(tool_args)
|
||||
except Exception as e:
|
||||
log.debug(e)
|
||||
return None
|
||||
tool_call.setdefault('function', {})['arguments'] = json.dumps(params)
|
||||
return params
|
||||
|
|
|
|||
|
|
@ -207,6 +207,7 @@ async def load_tool_module_by_id(tool_id, content=None):
|
|||
if not ENABLE_PLUGINS:
|
||||
raise RuntimeError('Plugins are disabled by ENABLE_PLUGINS=false')
|
||||
|
||||
frontmatter = None
|
||||
if content is None:
|
||||
tool = await Tools.get_tool_by_id(tool_id)
|
||||
if not tool:
|
||||
|
|
@ -238,7 +239,8 @@ async def load_tool_module_by_id(tool_id, content=None):
|
|||
|
||||
# Executing the modified content in the created module's namespace
|
||||
exec(content, module.__dict__)
|
||||
frontmatter = extract_frontmatter(content)
|
||||
if frontmatter is None:
|
||||
frontmatter = extract_frontmatter(content)
|
||||
log.info(f'Loaded module: {module.__name__}')
|
||||
|
||||
# Create and return the object if the class 'Tools' is found in the module
|
||||
|
|
@ -258,6 +260,7 @@ async def load_function_module_by_id(function_id: str, content: str | None = Non
|
|||
if not ENABLE_PLUGINS:
|
||||
raise RuntimeError('Plugins are disabled by ENABLE_PLUGINS=false')
|
||||
|
||||
frontmatter = None
|
||||
if content is None:
|
||||
function = await Functions.get_function_by_id(function_id)
|
||||
if not function:
|
||||
|
|
@ -286,7 +289,8 @@ async def load_function_module_by_id(function_id: str, content: str | None = Non
|
|||
|
||||
# Execute the modified content in the created module's namespace
|
||||
exec(content, module.__dict__)
|
||||
frontmatter = extract_frontmatter(content)
|
||||
if frontmatter is None:
|
||||
frontmatter = extract_frontmatter(content)
|
||||
log.info(f'Loaded module: {module.__name__}')
|
||||
|
||||
# Create appropriate object based on available class type in the module
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue