refac: async db

This commit is contained in:
Timothy Jaeryang Baek
2026-04-12 14:22:11 -05:00
parent b618d84065
commit 27169124f2
74 changed files with 4831 additions and 4479 deletions
@@ -11,7 +11,7 @@ from open_webui.models.access_grants import (
)
from open_webui.config import DEFAULT_USER_PERMISSIONS
from sqlalchemy.orm import Session
from sqlalchemy.ext.asyncio import AsyncSession
def fill_missing_permissions(permissions: dict[str, Any], default_permissions: dict[str, Any]) -> dict[str, Any]:
@@ -28,10 +28,10 @@ def fill_missing_permissions(permissions: dict[str, Any], default_permissions: d
return permissions
def get_permissions(
async def get_permissions(
user_id: str,
default_permissions: dict[str, Any],
db: Session | None = None,
db: AsyncSession | None = None,
) -> dict[str, Any]:
"""
Get all permissions for a user by combining the permissions of all groups the user is a member of.
@@ -53,7 +53,7 @@ def get_permissions(
permissions[key] = permissions[key] or value # Use the most permissive value (True > False)
return permissions
user_groups = Groups.get_groups_by_member_id(user_id, db=db)
user_groups = await Groups.get_groups_by_member_id(user_id, db=db)
# Deep copy default permissions to avoid modifying the original dict
permissions = json.loads(json.dumps(default_permissions))
@@ -68,11 +68,11 @@ def get_permissions(
return permissions
def has_permission(
async def has_permission(
user_id: str,
permission_key: str,
default_permissions: dict[str, Any] = {},
db: Session | None = None,
db: AsyncSession | None = None,
) -> bool:
"""
Check if a user has a specific permission by checking the group permissions
@@ -93,7 +93,7 @@ def has_permission(
permission_hierarchy = permission_key.split('.')
# Retrieve user group permissions
user_groups = Groups.get_groups_by_member_id(user_id, db=db)
user_groups = await Groups.get_groups_by_member_id(user_id, db=db)
for group in user_groups:
if get_permission(group.permissions or {}, permission_hierarchy):
@@ -104,12 +104,12 @@ def has_permission(
return get_permission(default_permissions, permission_hierarchy)
def has_access(
async def has_access(
user_id: str,
permission: str = 'read',
access_grants: list | None = None,
user_group_ids: set[str] | None = None,
db: Session | None = None,
db: AsyncSession | None = None,
) -> bool:
"""
Check if a user has the specified permission using an in-memory access_grants list.
@@ -126,7 +126,7 @@ def has_access(
return False
if user_group_ids is None:
user_groups = Groups.get_groups_by_member_id(user_id, db=db)
user_groups = await Groups.get_groups_by_member_id(user_id, db=db)
user_group_ids = {group.id for group in user_groups}
for grant in access_grants:
@@ -144,7 +144,7 @@ def has_access(
return False
def has_connection_access(
async def has_connection_access(
user: UserModel,
connection: dict,
user_group_ids: set[str] | None = None,
@@ -163,10 +163,10 @@ def has_connection_access(
return True
if user_group_ids is None:
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id)}
user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id)}
access_grants = (connection.get('config') or {}).get('access_grants', [])
return has_access(user.id, 'read', access_grants, user_group_ids)
return await has_access(user.id, 'read', access_grants, user_group_ids)
def migrate_access_control(data: dict, ac_key: str = 'access_control', grants_key: str = 'access_grants') -> None:
@@ -210,13 +210,13 @@ def migrate_access_control(data: dict, ac_key: str = 'access_control', grants_ke
data.pop(ac_key, None)
def filter_allowed_access_grants(
async def filter_allowed_access_grants(
default_permissions: dict[str, Any],
user_id: str,
user_role: str,
access_grants: list,
public_permission_key: str,
db: Session | None = None,
db: AsyncSession | None = None,
) -> list:
"""
Checks if the user has the required permissions to grant access to a resource.
@@ -228,7 +228,7 @@ def filter_allowed_access_grants(
# Check if user can share publicly
if (
has_public_read_access_grant(access_grants) or has_public_write_access_grant(access_grants)
) and not has_permission(
) and not await has_permission(
user_id,
public_permission_key,
default_permissions,
@@ -246,7 +246,7 @@ def filter_allowed_access_grants(
]
# Strip individual user sharing if user lacks permission
if has_user_access_grant(access_grants) and not has_permission(
if has_user_access_grant(access_grants) and not await has_permission(
user_id,
'access_grants.allow_users',
default_permissions,
@@ -257,7 +257,7 @@ def filter_allowed_access_grants(
return access_grants
def check_model_access(
async def check_model_access(
user: UserModel,
model_info,
bypass_filter: bool = False,
@@ -270,7 +270,7 @@ def check_model_access(
Args:
user: The authenticated user.
model_info: The model record from Models.get_model_by_id(),
model_info: The model record from await Models.get_model_by_id(),
or None if the model is not registered.
bypass_filter: If True, skip all access checks (used by
internal callers and BYPASS_MODEL_ACCESS_CONTROL).
@@ -284,10 +284,10 @@ def check_model_access(
if user.role == 'user':
from open_webui.models.access_grants import AccessGrants
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id)}
user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id)}
if not (
user.id == model_info.user_id
or AccessGrants.has_access(
or await AccessGrants.has_access(
user_id=user.id,
resource_type='model',
resource_id=model_info.id,
@@ -9,16 +9,16 @@ from open_webui.models.groups import Groups
from open_webui.models.models import Models
from open_webui.models.access_grants import AccessGrants
from sqlalchemy.orm import Session
from sqlalchemy.ext.asyncio import AsyncSession
log = logging.getLogger(__name__)
def has_access_to_file(
async def has_access_to_file(
file_id: str | None,
access_type: str,
user: UserModel,
db: Session | None = None,
db: AsyncSession | None = None,
) -> bool:
"""
Check if a user has the specified access to a file through any of:
@@ -30,7 +30,7 @@ def has_access_to_file(
NOTE: This does NOT check direct file ownership — callers should check
file.user_id == user.id separately before calling this.
"""
file = Files.get_file_by_id(file_id, db=db)
file = await Files.get_file_by_id(file_id, db=db)
log.debug(f'Checking if user has {access_type} access to file')
if not file:
return False
@@ -40,10 +40,10 @@ def has_access_to_file(
return True
# Check if the file is associated with any knowledge bases the user has access to
knowledge_bases = Knowledges.get_knowledges_by_file_id(file_id, db=db)
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id, db=db)}
knowledge_bases = await Knowledges.get_knowledges_by_file_id(file_id, db=db)
user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)}
for knowledge_base in knowledge_bases:
if knowledge_base.user_id == user.id or AccessGrants.has_access(
if knowledge_base.user_id == user.id or await AccessGrants.has_access(
user_id=user.id,
resource_type='knowledge',
resource_id=knowledge_base.id,
@@ -55,24 +55,24 @@ def has_access_to_file(
knowledge_base_id = file.meta.get('collection_name') if file.meta else None
if knowledge_base_id:
knowledge_bases = Knowledges.get_knowledge_bases_by_user_id(user.id, access_type, db=db)
knowledge_bases = await Knowledges.get_knowledge_bases_by_user_id(user.id, access_type, db=db)
for knowledge_base in knowledge_bases:
if knowledge_base.id == knowledge_base_id:
return True
# Check if the file is associated with any channels the user has access to
channels = Channels.get_channels_by_file_id_and_user_id(file_id, user.id, db=db)
channels = await Channels.get_channels_by_file_id_and_user_id(file_id, user.id, db=db)
if access_type == 'read' and channels:
return True
# Check if the file is associated with any chats the user has access to
# TODO: Granular access control for chats
chats = Chats.get_shared_chats_by_file_id(file_id, db=db)
chats = await Chats.get_shared_chats_by_file_id(file_id, db=db)
if chats:
return True
# Check if the file is directly attached to a shared workspace model
for model in Models.get_models_by_user_id(user.id, permission=access_type, db=db):
for model in await Models.get_models_by_user_id(user.id, permission=access_type, db=db):
knowledge_items = getattr(model.meta, 'knowledge', None) or []
for item in knowledge_items:
if isinstance(item, dict) and item.get('type') == 'file' and item.get('id') == file.id:
+7 -7
View File
@@ -26,7 +26,7 @@ async def chat_action(request: Request, action_id: str, form_data: dict, user: A
else:
sub_action_id = None
action = Functions.get_function_by_id(action_id)
action = await Functions.get_function_by_id(action_id)
if not action:
raise Exception(f'Action not found: {action_id}')
@@ -47,7 +47,7 @@ async def chat_action(request: Request, action_id: str, form_data: dict, user: A
raise Exception('Model not found')
model = models[model_id]
__event_emitter__ = get_event_emitter(
__event_emitter__ = await get_event_emitter(
{
'chat_id': data['chat_id'],
'message_id': data['id'],
@@ -55,7 +55,7 @@ async def chat_action(request: Request, action_id: str, form_data: dict, user: A
'user_id': user.id,
}
)
__event_call__ = get_event_call(
__event_call__ = await get_event_call(
{
'chat_id': data['chat_id'],
'message_id': data['id'],
@@ -64,10 +64,10 @@ async def chat_action(request: Request, action_id: str, form_data: dict, user: A
}
)
function_module, _, _ = get_function_module_from_cache(request, action_id)
function_module, _, _ = await get_function_module_from_cache(request, action_id)
if hasattr(function_module, 'valves') and hasattr(function_module, 'Valves'):
valves = Functions.get_function_valves_by_id(action_id)
valves = await Functions.get_function_valves_by_id(action_id)
function_module.valves = function_module.Valves(**(valves if valves else {}))
if hasattr(function_module, 'action'):
@@ -98,7 +98,7 @@ async def chat_action(request: Request, action_id: str, form_data: dict, user: A
try:
if hasattr(function_module, 'UserValves'):
__user__['valves'] = function_module.UserValves(
**Functions.get_user_valves_by_id_and_user_id(action_id, user.id)
**await Functions.get_user_valves_by_id_and_user_id(action_id, user.id)
)
except Exception as e:
log.exception(f'Failed to get user values: {e}')
@@ -111,7 +111,7 @@ async def chat_action(request: Request, action_id: str, form_data: dict, user: A
data = action(**params)
# Process action result for Rich UI embeds (HTMLResponse, tuple with headers)
processed_result, _, action_embeds = process_tool_result(
processed_result, _, action_embeds = await process_tool_result(
request,
action_id,
data,
+13 -13
View File
@@ -324,7 +324,7 @@ async def get_current_user(
# auth by api key
if token.startswith('sk-'):
user = get_current_user_by_api_key(request, token)
user = await get_current_user_by_api_key(request, token)
# Add user info to current span
if ENABLE_OTEL:
@@ -356,7 +356,7 @@ async def get_current_user(
detail='Invalid token',
)
user = Users.get_user_by_id(data['id'])
user = await Users.get_user_by_id(data['id'])
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -382,10 +382,10 @@ async def get_current_user(
current_span.set_attribute('client.user.role', user.role)
current_span.set_attribute('client.auth.type', 'jwt')
# Refresh the user's last active timestamp asynchronously
# to prevent blocking the request
if background_tasks:
background_tasks.add_task(Users.update_last_active_by_id, user.id)
# Refresh the user's last active timestamp
# Fire-and-forget via asyncio.create_task to avoid blocking
import asyncio
asyncio.create_task(Users.update_last_active_by_id(user.id))
return user
else:
raise HTTPException(
@@ -407,9 +407,9 @@ async def get_current_user(
raise e
def get_current_user_by_api_key(request, api_key: str):
async def get_current_user_by_api_key(request, api_key: str):
# Each function call manages its own short-lived session internally
user = Users.get_user_by_api_key(api_key)
user = await Users.get_user_by_api_key(api_key)
if user is None:
raise HTTPException(
@@ -419,7 +419,7 @@ def get_current_user_by_api_key(request, api_key: str):
if not request.state.enable_api_keys or (
user.role != 'admin'
and not has_permission(
and not await has_permission(
user.id,
'features.api_keys',
request.app.state.config.USER_PERMISSIONS,
@@ -438,7 +438,7 @@ def get_current_user_by_api_key(request, api_key: str):
current_span.set_attribute('client.user.role', user.role)
current_span.set_attribute('client.auth.type', 'api_key')
Users.update_last_active_by_id(user.id)
await Users.update_last_active_by_id(user.id)
return user
@@ -460,7 +460,7 @@ def get_admin_user(user=Depends(get_current_user)):
return user
def create_admin_user(email: str, password: str, name: str = 'Admin'):
async def create_admin_user(email: str, password: str, name: str = 'Admin'):
"""
Create an admin user from environment variables.
Used for headless/automated deployments.
@@ -470,14 +470,14 @@ def create_admin_user(email: str, password: str, name: str = 'Admin'):
if not email or not password:
return None
if Users.has_users():
if await Users.has_users():
log.debug('Users already exist, skipping admin creation')
return None
log.info(f'Creating admin account from environment variables: {email}')
try:
hashed = get_password_hash(password)
user = Auths.insert_new_auth(
user = await Auths.insert_new_auth(
email=email.lower(),
password=hashed,
name=name,
+9 -9
View File
@@ -126,7 +126,7 @@ async def automation_worker_loop(app) -> None:
while True:
try:
with get_db() as db:
batch = Automations.claim_due(int(time.time_ns()), limit=10, db=db)
batch = await Automations.claim_due(int(time.time_ns()), limit=10, db=db)
if batch:
log.info(f'Claimed {len(batch)} due automation(s)')
for automation in batch:
@@ -283,9 +283,9 @@ async def execute_automation(app, automation: AutomationModel) -> None:
(filters, model params, knowledge/RAG, tools, DB saves, webhooks).
"""
try:
user = Users.get_user_by_id(automation.user_id)
user = await Users.get_user_by_id(automation.user_id)
if not user:
_record_run(automation.id, 'error', error='User not found')
await _record_run(automation.id, 'error', error='User not found')
return
prompt = prompt_template(automation.data['prompt'], user)
@@ -297,7 +297,7 @@ async def execute_automation(app, automation: AutomationModel) -> None:
assistant_msg_id = str(uuid4())
# Create the chat with user message (same structure as frontend)
chat = Chats.insert_new_chat(
chat = await Chats.insert_new_chat(
automation.user_id,
ChatForm(
chat={
@@ -336,7 +336,7 @@ async def execute_automation(app, automation: AutomationModel) -> None:
)
if not chat:
_record_run(automation.id, 'error', error='Failed to create chat')
await _record_run(automation.id, 'error', error='Failed to create chat')
return
# Notify frontend to refresh chat list
@@ -404,11 +404,11 @@ async def execute_automation(app, automation: AutomationModel) -> None:
room=f'user:{automation.user_id}',
)
_record_run(automation.id, 'success', chat_id=chat.id)
await _record_run(automation.id, 'success', chat_id=chat.id)
except Exception as e:
log.exception(f'Automation {automation.id} failed')
_record_run(automation.id, 'error', error=str(e)[:4000])
await _record_run(automation.id, 'error', error=str(e)[:4000])
####################
@@ -416,7 +416,7 @@ async def execute_automation(app, automation: AutomationModel) -> None:
####################
def _record_run(
async def _record_run(
automation_id: str,
status: str,
chat_id: str = None,
@@ -424,4 +424,4 @@ def _record_run(
):
"""Insert a run record into automation_run."""
with get_db() as db:
AutomationRuns.insert(automation_id, status, chat_id=chat_id, error=error, db=db)
await AutomationRuns.insert(automation_id, status, chat_id=chat_id, error=error, db=db)
+6 -6
View File
@@ -72,7 +72,7 @@ async def generate_direct_chat_completion(
session_id = metadata.get('session_id')
request_id = str(uuid.uuid4()) # Generate a unique request ID
event_caller = get_event_call(metadata)
event_caller = await get_event_call(metadata)
channel = f'{user_id}:{session_id}:{request_id}'
logging.info(f'WebSocket channel: {channel}')
@@ -199,7 +199,7 @@ async def generate_chat_completion(
# Check if user has access to the model
if not bypass_filter and user.role == 'user':
try:
check_model_access(user, model)
await check_model_access(user, model)
except Exception as e:
raise e
@@ -343,8 +343,8 @@ async def chat_completed(request: Request, form_data: dict, user: Any):
}
extra_params = {
'__event_emitter__': get_event_emitter(metadata),
'__event_call__': get_event_call(metadata),
'__event_emitter__': await get_event_emitter(metadata),
'__event_call__': await get_event_call(metadata),
'__user__': user.model_dump() if isinstance(user, UserModel) else {},
'__metadata__': metadata,
'__request__': request,
@@ -352,8 +352,8 @@ async def chat_completed(request: Request, form_data: dict, user: Any):
}
try:
filter_ids = get_sorted_filter_ids(request, model, metadata.get('filter_ids', []))
filter_functions = Functions.get_functions_by_ids(filter_ids)
filter_ids = await get_sorted_filter_ids(request, model, metadata.get('filter_ids', []))
filter_functions = await Functions.get_functions_by_ids(filter_ids)
result, _ = await process_filter_functions(
request=request,
+1 -1
View File
@@ -68,7 +68,7 @@ async def generate_embeddings(
# Access filtering
if not getattr(request.state, 'direct', False):
if not bypass_filter and user.role == 'user':
check_model_access(user, model)
await check_model_access(user, model)
# Ollama backend — use /api/embed which supports batch input natively
if model.get('owned_by') == 'ollama':
+32 -23
View File
@@ -31,7 +31,7 @@ BASE64_IMAGE_URL_PREFIX = re.compile(r'data:image/\w+;base64,', re.IGNORECASE)
MARKDOWN_IMAGE_URL_PATTERN = re.compile(r'!\[(.*?)\]\((.+?)\)', re.IGNORECASE)
def get_image_base64_from_url(url: str) -> Optional[str]:
async def get_image_base64_from_url(url: str) -> Optional[str]:
try:
if url.startswith('http'):
# Validate URL to prevent SSRF attacks against local/private networks
@@ -44,7 +44,7 @@ def get_image_base64_from_url(url: str) -> Optional[str]:
content_type = response.headers.get('Content-Type', 'image/png')
return f'data:{content_type};base64,{encoded_string}'
else:
file = Files.get_file_by_id(url)
file = await Files.get_file_by_id(url)
if not file:
return None
@@ -64,13 +64,13 @@ def get_image_base64_from_url(url: str) -> Optional[str]:
return None
def get_image_url_from_base64(request, base64_image_string, metadata, user):
async def get_image_url_from_base64(request, base64_image_string, metadata, user):
if BASE64_IMAGE_URL_PREFIX.match(base64_image_string):
image_url = ''
# Extract base64 image data from the line
image_data, content_type = get_image_data(base64_image_string)
if image_data is not None:
_, image_url = upload_image(
_, image_url = await upload_image(
request,
image_data,
content_type,
@@ -82,17 +82,26 @@ def get_image_url_from_base64(request, base64_image_string, metadata, user):
return None
def convert_markdown_base64_images(request, content: str, metadata, user):
def replace(match):
base64_string = match.group(2)
MIN_REPLACEMENT_URL_LENGTH = 1024
if len(base64_string) > MIN_REPLACEMENT_URL_LENGTH:
url = get_image_url_from_base64(request, base64_string, metadata, user)
if url:
return f'![{match.group(1)}]({url})'
return match.group(0)
async def convert_markdown_base64_images(request, content: str, metadata, user):
MIN_REPLACEMENT_URL_LENGTH = 1024
result_parts = []
last_end = 0
return MARKDOWN_IMAGE_URL_PATTERN.sub(replace, content)
for match in MARKDOWN_IMAGE_URL_PATTERN.finditer(content):
result_parts.append(content[last_end:match.start()])
base64_string = match.group(2)
if len(base64_string) > MIN_REPLACEMENT_URL_LENGTH:
url = await get_image_url_from_base64(request, base64_string, metadata, user)
if url:
result_parts.append(f'![{match.group(1)}]({url})')
else:
result_parts.append(match.group(0))
else:
result_parts.append(match.group(0))
last_end = match.end()
result_parts.append(content[last_end:])
return ''.join(result_parts)
def load_b64_audio_data(b64_str):
@@ -110,7 +119,7 @@ def load_b64_audio_data(b64_str):
return None, None
def upload_audio(request, audio_data, content_type, metadata, user):
async def upload_audio(request, audio_data, content_type, metadata, user):
audio_format = mimetypes.guess_extension(content_type)
file = UploadFile(
file=io.BytesIO(audio_data),
@@ -119,7 +128,7 @@ def upload_audio(request, audio_data, content_type, metadata, user):
'content-type': content_type,
},
)
file_item = upload_file_handler(
file_item = await upload_file_handler(
request,
file=file,
metadata=metadata,
@@ -130,13 +139,13 @@ def upload_audio(request, audio_data, content_type, metadata, user):
return url
def get_audio_url_from_base64(request, base64_audio_string, metadata, user):
async def get_audio_url_from_base64(request, base64_audio_string, metadata, user):
if 'data:audio/wav;base64' in base64_audio_string:
audio_url = ''
# Extract base64 audio data from the line
audio_data, content_type = load_b64_audio_data(base64_audio_string)
if audio_data is not None:
audio_url = upload_audio(
audio_url = await upload_audio(
request,
audio_data,
content_type,
@@ -147,16 +156,16 @@ def get_audio_url_from_base64(request, base64_audio_string, metadata, user):
return None
def get_file_url_from_base64(request, base64_file_string, metadata, user):
async def get_file_url_from_base64(request, base64_file_string, metadata, user):
if BASE64_IMAGE_URL_PREFIX.match(base64_file_string):
return get_image_url_from_base64(request, base64_file_string, metadata, user)
return await get_image_url_from_base64(request, base64_file_string, metadata, user)
elif 'data:audio/wav;base64' in base64_file_string:
return get_audio_url_from_base64(request, base64_file_string, metadata, user)
return await get_audio_url_from_base64(request, base64_file_string, metadata, user)
return None
def get_image_base64_from_file_id(id: str) -> Optional[str]:
file = Files.get_file_by_id(id)
async def get_image_base64_from_file_id(id: str) -> Optional[str]:
file = await Files.get_file_by_id(id)
if not file:
return None
+24 -15
View File
@@ -10,44 +10,53 @@ from open_webui.models.functions import Functions
log = logging.getLogger(__name__)
def get_function_module(request, function_id, load_from_db=True):
async def get_function_module(request, function_id, load_from_db=True):
"""
Get the function module by its ID.
"""
function_module, _, _ = get_function_module_from_cache(request, function_id, load_from_db)
function_module, _, _ = await get_function_module_from_cache(request, function_id, load_from_db)
return function_module
def get_sorted_filter_ids(request, model: dict, enabled_filter_ids: list = None):
def get_priority(function_id):
async def get_sorted_filter_ids(request, model: dict, enabled_filter_ids: list = None):
async def get_priority(function_id):
try:
function_module = get_function_module(request, function_id)
function_module = await get_function_module(request, function_id)
if function_module and hasattr(function_module, 'Valves'):
valves_db = Functions.get_function_valves_by_id(function_id)
valves_db = await Functions.get_function_valves_by_id(function_id)
valves = function_module.Valves(**(valves_db if valves_db else {}))
return getattr(valves, 'priority', 0)
except Exception:
pass
return 0
filter_ids = [function.id for function in Functions.get_global_filter_functions()]
filter_ids = [function.id for function in await Functions.get_global_filter_functions()]
if 'info' in model and 'meta' in model['info']:
filter_ids.extend(model['info']['meta'].get('filterIds', []))
filter_ids = list(set(filter_ids))
active_filter_ids = {function.id for function in Functions.get_functions_by_type('filter', active_only=True)}
active_filter_ids = {function.id for function in await Functions.get_functions_by_type('filter', active_only=True)}
def get_active_status(filter_id):
function_module = get_function_module(request, filter_id)
async def get_active_status(filter_id):
function_module = await get_function_module(request, filter_id)
if getattr(function_module, 'toggle', None):
return filter_id in (enabled_filter_ids or set())
return True
active_filter_ids = {filter_id for filter_id in active_filter_ids if get_active_status(filter_id)}
# Pre-compute active status for each filter (async functions can't be used in set comprehensions)
resolved_active = {}
for filter_id in active_filter_ids:
resolved_active[filter_id] = await get_active_status(filter_id)
active_filter_ids = {fid for fid, is_active in resolved_active.items() if is_active}
filter_ids = [fid for fid in filter_ids if fid in active_filter_ids]
filter_ids.sort(key=lambda fid: (get_priority(fid), fid))
# Pre-compute priorities (async functions can't be used in sort keys)
priorities = {}
for fid in filter_ids:
priorities[fid] = await get_priority(fid)
filter_ids.sort(key=lambda fid: (priorities.get(fid, 0), fid))
return filter_ids
@@ -63,7 +72,7 @@ async def process_filter_functions(request, filter_functions, filter_type, form_
if not filter:
continue
function_module = get_function_module(request, filter_id, load_from_db=(filter_type != 'stream'))
function_module = await get_function_module(request, filter_id, load_from_db=(filter_type != 'stream'))
# Prepare handler function
handler = getattr(function_module, filter_type, None)
if not handler:
@@ -75,7 +84,7 @@ async def process_filter_functions(request, filter_functions, filter_type, form_
# Apply valves to the function
if hasattr(function_module, 'valves') and hasattr(function_module, 'Valves'):
valves = Functions.get_function_valves_by_id(filter_id)
valves = await Functions.get_function_valves_by_id(filter_id)
function_module.valves = function_module.Valves(**(valves if valves else {}))
try:
@@ -100,7 +109,7 @@ async def process_filter_functions(request, filter_functions, filter_type, form_
if hasattr(function_module, 'UserValves'):
try:
params['__user__']['valves'] = function_module.UserValves(
**Functions.get_user_valves_by_id_and_user_id(filter_id, params['__user__']['id'])
**await Functions.get_user_valves_by_id_and_user_id(filter_id, params['__user__']['id'])
)
except Exception as e:
log.exception(f'Failed to get user values: {e}')
+2 -2
View File
@@ -4,7 +4,7 @@ from open_webui.models.groups import Groups
log = logging.getLogger(__name__)
def apply_default_group_assignment(
async def apply_default_group_assignment(
default_group_id: str,
user_id: str,
db=None,
@@ -18,6 +18,6 @@ def apply_default_group_assignment(
"""
if default_group_id:
try:
Groups.add_users_to_group(default_group_id, [user_id], db=db)
await Groups.add_users_to_group(default_group_id, [user_id], db=db)
except Exception as e:
log.error(f'Failed to add user {user_id} to default group {default_group_id}: {e}')
+1 -1
View File
@@ -44,7 +44,7 @@ def create_httpx_client(headers=None, timeout=None, auth=None):
return _build_httpx_client(headers=headers, timeout=timeout, auth=auth, verify=True)
def create_insecure_httpx_client(headers=None, timeout=None, auth=None):
async def create_insecure_httpx_client(headers=None, timeout=None, auth=None):
return _build_httpx_client(headers=headers, timeout=timeout, auth=auth, verify=False)
+69 -68
View File
@@ -936,7 +936,7 @@ def apply_source_context_to_messages(
)
def process_tool_result(
async def process_tool_result(
request,
tool_function_name,
tool_result,
@@ -1075,7 +1075,7 @@ def process_tool_result(
pass
tool_response.append(text)
elif item.get('type') in ['image', 'audio']:
file_url = get_file_url_from_base64(
file_url = await get_file_url_from_base64(
request,
f'data:{item.get("mimeType")};base64,{item.get("data", item.get("blob", ""))}',
{
@@ -1304,7 +1304,7 @@ async def chat_completion_tools_handler(
except Exception as e:
tool_result = str(e)
tool_result, tool_result_files, tool_result_embeds = process_tool_result(
tool_result, tool_result_files, tool_result_embeds = await process_tool_result(
request,
tool_function_name,
tool_result,
@@ -1602,7 +1602,7 @@ def get_images_from_messages(message_list):
return images
def get_image_urls(delta_images, request, metadata, user) -> list[str]:
async def get_image_urls(delta_images, request, metadata, user) -> list[str]:
if not isinstance(delta_images, list):
return []
@@ -1616,21 +1616,21 @@ def get_image_urls(delta_images, request, metadata, user) -> list[str]:
continue
if url.startswith('data:image/png;base64'):
url = get_image_url_from_base64(request, url, metadata, user)
url = await get_image_url_from_base64(request, url, metadata, user)
image_urls.append(url)
return image_urls
def add_file_context(messages: list, chat_id: str, user) -> list:
async def add_file_context(messages: list, chat_id: str, user) -> list:
"""
Add file URLs to messages for native function calling.
"""
if not chat_id or chat_id.startswith('local:'):
return messages
chat = Chats.get_chat_by_id_and_user_id(chat_id, user.id)
chat = await Chats.get_chat_by_id_and_user_id(chat_id, user.id)
if not chat:
return messages
@@ -1686,7 +1686,7 @@ async def chat_image_generation_handler(request: Request, form_data: dict, extra
if chat_id.startswith('local:'):
message_list = form_data.get('messages', [])
else:
chat = Chats.get_chat_by_id_and_user_id(chat_id, user.id)
chat = await Chats.get_chat_by_id_and_user_id(chat_id, user.id)
await __event_emitter__(
{
'type': 'status',
@@ -2066,12 +2066,12 @@ async def convert_url_images_to_base64(form_data):
return form_data
def load_messages_from_db(chat_id: str, message_id: str) -> Optional[list[dict]]:
async def load_messages_from_db(chat_id: str, message_id: str) -> Optional[list[dict]]:
"""
Load the message chain from DB up to message_id,
keeping only LLM-relevant fields (role, content, output).
"""
messages_map = Chats.get_messages_map_by_chat_id(chat_id)
messages_map = await Chats.get_messages_map_by_chat_id(chat_id)
if not messages_map:
return None
@@ -2149,7 +2149,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
parent_message_id = metadata.get('parent_message_id')
if chat_id and parent_message_id and not chat_id.startswith('local:'):
db_messages = load_messages_from_db(chat_id, parent_message_id)
db_messages = await load_messages_from_db(chat_id, parent_message_id)
if db_messages:
system_message = get_system_message(form_data.get('messages', []))
form_data['messages'] = [system_message, *db_messages] if system_message else db_messages
@@ -2192,8 +2192,8 @@ async def process_chat_payload(request, form_data, user, metadata, model):
form_data = await convert_url_images_to_base64(form_data)
event_emitter = get_event_emitter(metadata)
event_caller = get_event_call(metadata)
event_emitter = await get_event_emitter(metadata)
event_caller = await get_event_call(metadata)
extra_params = {
'__event_emitter__': event_emitter,
@@ -2231,14 +2231,14 @@ async def process_chat_payload(request, form_data, user, metadata, model):
chat_id = metadata.get('chat_id', None)
folder_id = None
if chat_id and user:
folder_id = Chats.get_chat_folder_id(chat_id, user.id)
folder_id = await Chats.get_chat_folder_id(chat_id, user.id)
# Fallback: use folder_id from metadata (temporary chats have no DB record)
if not folder_id:
folder_id = metadata.get('folder_id', None)
if folder_id and user:
folder = Folders.get_folder_by_id_and_user_id(folder_id, user.id)
folder = await Folders.get_folder_by_id_and_user_id(folder_id, user.id)
if folder and folder.data:
if 'system_prompt' in folder.data:
@@ -2305,8 +2305,8 @@ async def process_chat_payload(request, form_data, user, metadata, model):
raise e
try:
filter_ids = get_sorted_filter_ids(request, model, metadata.get('filter_ids', []))
filter_functions = Functions.get_functions_by_ids(filter_ids)
filter_ids = await get_sorted_filter_ids(request, model, metadata.get('filter_ids', []))
filter_functions = await Functions.get_functions_by_ids(filter_ids)
form_data, flags = await process_filter_functions(
request=request,
@@ -2399,12 +2399,13 @@ async def process_chat_payload(request, form_data, user, metadata, model):
if all_skill_ids:
from open_webui.models.skills import Skills as SkillsModel
accessible_skill_ids = {s.id for s in SkillsModel.get_skills_by_user_id(user.id, 'read')}
available_skills = [
s
for sid in all_skill_ids
if sid in accessible_skill_ids and (s := SkillsModel.get_skill_by_id(sid)) and s.is_active
]
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)
if s and s.is_active:
available_skills.append(s)
skill_descriptions = ''
for skill in available_skills:
@@ -2441,7 +2442,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
# Get folder files
folder_id = file_item.get('id', None)
if folder_id:
folder = Folders.get_folder_by_id_and_user_id(folder_id, user.id)
folder = await Folders.get_folder_by_id_and_user_id(folder_id, user.id)
if folder and folder.data and 'files' in folder.data:
files = [f for f in files if f.get('id', None) != folder_id]
files = [*files, *folder.data['files']]
@@ -2495,7 +2496,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
continue
# Check access control for MCP server
if not has_connection_access(user, mcp_server_connection):
if not await has_connection_access(user, mcp_server_connection):
log.warning(f'Access denied to MCP server {server_id} for user {user.id}')
continue
@@ -2556,7 +2557,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
tool_specs = await mcp_clients[server_id].list_tool_specs()
for tool_spec in tool_specs:
def make_tool_function(client, function_name):
async def make_tool_function(client, function_name):
async def tool_function(**kwargs):
return await client.call_tool(
function_name,
@@ -2570,7 +2571,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
# Skip this function
continue
tool_function = make_tool_function(mcp_clients[server_id], tool_spec['name'])
tool_function = await make_tool_function(mcp_clients[server_id], tool_spec['name'])
mcp_tools_dict[f'{server_id}_{tool_spec["name"]}'] = {
'spec': {
@@ -2664,8 +2665,8 @@ async def process_chat_payload(request, form_data, user, metadata, model):
if metadata.get('params', {}).get('function_calling') == 'native' and builtin_tools_enabled:
# Add file context to user messages
chat_id = metadata.get('chat_id')
form_data['messages'] = add_file_context(form_data.get('messages', []), chat_id, user)
builtin_tools = get_builtin_tools(
form_data['messages'] = await add_file_context(form_data.get('messages', []), chat_id, user)
builtin_tools = await get_builtin_tools(
request,
{
**extra_params,
@@ -2755,7 +2756,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
return form_data, metadata, events
def get_event_emitter_and_caller(metadata):
async def get_event_emitter_and_caller(metadata):
event_emitter = None
event_caller = None
@@ -2763,18 +2764,18 @@ def get_event_emitter_and_caller(metadata):
# It broadcasts to user:{user_id} room AND persists to DB,
# so it works for backend-initiated calls (automations, API).
if metadata.get('chat_id') and metadata.get('message_id'):
event_emitter = get_event_emitter(metadata)
event_emitter = await get_event_emitter(metadata)
# event_caller needs session_id — it calls back to a specific
# websocket session (used by direct tools, pyodide code interpreter).
if metadata.get('session_id') and metadata.get('chat_id') and metadata.get('message_id'):
event_caller = get_event_call(metadata)
event_caller = await get_event_call(metadata)
return event_emitter, event_caller
def build_chat_response_context(request, form_data, user, model, metadata, tasks, events):
event_emitter, event_caller = get_event_emitter_and_caller(metadata)
async def build_chat_response_context(request, form_data, user, model, metadata, tasks, events):
event_emitter, event_caller = await get_event_emitter_and_caller(metadata)
return {
'request': request,
'form_data': form_data,
@@ -2862,7 +2863,7 @@ async def background_tasks_handler(ctx):
messages = []
if 'chat_id' in metadata and not metadata['chat_id'].startswith('local:'):
messages_map = Chats.get_messages_map_by_chat_id(metadata['chat_id'])
messages_map = await Chats.get_messages_map_by_chat_id(metadata['chat_id'])
message = messages_map.get(metadata['message_id']) if messages_map else None
message_list = get_message_list(messages_map, metadata['message_id'])
@@ -2942,7 +2943,7 @@ async def background_tasks_handler(ctx):
)
if not metadata.get('chat_id', '').startswith('local:'):
Chats.upsert_message_to_chat_by_id_and_message_id(
await Chats.upsert_message_to_chat_by_id_and_message_id(
metadata['chat_id'],
metadata['message_id'],
{
@@ -2995,7 +2996,7 @@ async def background_tasks_handler(ctx):
if not title:
title = messages[0].get('content', user_message)
Chats.update_chat_title_by_id(metadata['chat_id'], title)
await Chats.update_chat_title_by_id(metadata['chat_id'], title)
await event_emitter(
{
@@ -3007,7 +3008,7 @@ async def background_tasks_handler(ctx):
if title == None and len(messages) == 2 and (not messages_map or len(messages_map) <= 2):
title = messages[0].get('content', user_message)
Chats.update_chat_title_by_id(metadata['chat_id'], title)
await Chats.update_chat_title_by_id(metadata['chat_id'], title)
await event_emitter(
{
@@ -3041,7 +3042,7 @@ async def background_tasks_handler(ctx):
try:
tags = json.loads(tags_string).get('tags', [])
Chats.update_chat_tags_by_id(metadata['chat_id'], tags, user)
await Chats.update_chat_tags_by_id(metadata['chat_id'], tags, user)
await event_emitter(
{
@@ -3076,7 +3077,7 @@ async def non_streaming_chat_response_handler(response, ctx):
else:
error = str(error)
Chats.upsert_message_to_chat_by_id_and_message_id(
await Chats.upsert_message_to_chat_by_id_and_message_id(
metadata['chat_id'],
metadata['message_id'],
{
@@ -3092,7 +3093,7 @@ async def non_streaming_chat_response_handler(response, ctx):
)
if 'selected_model_id' in response_data:
Chats.upsert_message_to_chat_by_id_and_message_id(
await Chats.upsert_message_to_chat_by_id_and_message_id(
metadata['chat_id'],
metadata['message_id'],
{
@@ -3112,7 +3113,7 @@ async def non_streaming_chat_response_handler(response, ctx):
}
)
title = Chats.get_chat_title_by_id(metadata['chat_id'])
title = await Chats.get_chat_title_by_id(metadata['chat_id'])
# Use output from backend if provided (OR-compliant backends),
# otherwise generate from response content
@@ -3143,7 +3144,7 @@ async def non_streaming_chat_response_handler(response, ctx):
# Save message in the database
usage = normalize_usage(response_data.get('usage', {}) or {})
Chats.upsert_message_to_chat_by_id_and_message_id(
await Chats.upsert_message_to_chat_by_id_and_message_id(
metadata['chat_id'],
metadata['message_id'],
{
@@ -3156,8 +3157,8 @@ async def non_streaming_chat_response_handler(response, ctx):
)
# Send a webhook notification if the user is not active
if request.app.state.config.ENABLE_USER_WEBHOOKS and not Users.is_user_active(user.id):
webhook_url = Users.get_user_webhook_url_by_id(user.id)
if request.app.state.config.ENABLE_USER_WEBHOOKS and not await Users.is_user_active(user.id):
webhook_url = await Users.get_user_webhook_url_by_id(user.id)
if webhook_url:
await post_webhook(
request.app.state.WEBUI_NAME,
@@ -3211,8 +3212,8 @@ async def streaming_chat_response_handler(response, ctx):
}
filter_functions = [
Functions.get_function_by_id(filter_id)
for filter_id in get_sorted_filter_ids(request, model, metadata.get('filter_ids', []))
await Functions.get_function_by_id(filter_id)
for filter_id in await get_sorted_filter_ids(request, model, metadata.get('filter_ids', []))
]
# Standard streaming response handler
@@ -3447,7 +3448,7 @@ async def streaming_chat_response_handler(response, ctx):
return output, end_flag
message = Chats.get_message_by_id_and_message_id(metadata['chat_id'], metadata['message_id'])
message = await Chats.get_message_by_id_and_message_id(metadata['chat_id'], metadata['message_id'])
tool_calls = []
@@ -3509,7 +3510,7 @@ async def streaming_chat_response_handler(response, ctx):
)
# Save message in the database
Chats.upsert_message_to_chat_by_id_and_message_id(
await Chats.upsert_message_to_chat_by_id_and_message_id(
metadata['chat_id'],
metadata['message_id'],
{
@@ -3579,7 +3580,7 @@ async def streaming_chat_response_handler(response, ctx):
if 'selected_model_id' in data:
model_id = data['selected_model_id']
Chats.upsert_message_to_chat_by_id_and_message_id(
await Chats.upsert_message_to_chat_by_id_and_message_id(
metadata['chat_id'],
metadata['message_id'],
{
@@ -3645,7 +3646,7 @@ async def streaming_chat_response_handler(response, ctx):
error = data.get('error', {})
if error:
try:
Chats.upsert_message_to_chat_by_id_and_message_id(
await Chats.upsert_message_to_chat_by_id_and_message_id(
metadata['chat_id'],
metadata['message_id'],
{
@@ -3762,10 +3763,10 @@ async def streaming_chat_response_handler(response, ctx):
}
)
image_urls = get_image_urls(delta.get('images', []), request, metadata, user)
image_urls = await get_image_urls(delta.get('images', []), request, metadata, user)
if image_urls:
image_file_list = [{'type': 'image', 'url': url} for url in image_urls]
message_files = Chats.add_message_files_by_id_and_message_id(
message_files = await Chats.add_message_files_by_id_and_message_id(
metadata['chat_id'],
metadata['message_id'],
image_file_list,
@@ -3847,7 +3848,7 @@ async def streaming_chat_response_handler(response, ctx):
)
if ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION:
value = convert_markdown_base64_images(
value = await convert_markdown_base64_images(
request,
value,
{
@@ -3963,7 +3964,7 @@ async def streaming_chat_response_handler(response, ctx):
if ENABLE_REALTIME_CHAT_SAVE:
# Save message in the database
Chats.upsert_message_to_chat_by_id_and_message_id(
await Chats.upsert_message_to_chat_by_id_and_message_id(
metadata['chat_id'],
metadata['message_id'],
{
@@ -4184,7 +4185,7 @@ async def streaming_chat_response_handler(response, ctx):
)
else:
tool_function = get_updated_tool_function(
tool_function = await get_updated_tool_function(
function=tool['callable'],
extra_params={
'__messages__': form_data.get('messages', []),
@@ -4197,7 +4198,7 @@ async def streaming_chat_response_handler(response, ctx):
except Exception as e:
tool_result = str(e)
tool_result, tool_result_files, tool_result_embeds = process_tool_result(
tool_result, tool_result_files, tool_result_embeds = await process_tool_result(
request,
tool_function_name,
tool_result,
@@ -4487,7 +4488,7 @@ async def streaming_chat_response_handler(response, ctx):
BLOCKED_MODULES = {CODE_INTERPRETER_BLOCKED_MODULES}
_real_import = builtins.__import__
def restricted_import(name, globals=None, locals=None, fromlist=(), level=0):
async def restricted_import(name, globals=None, locals=None, fromlist=(), level=0):
if name.split('.')[0] in BLOCKED_MODULES:
importer_name = globals.get('__name__') if globals else None
if importer_name == '__main__':
@@ -4541,7 +4542,7 @@ async def streaming_chat_response_handler(response, ctx):
stdoutLines = stdout.split('\n')
for idx, line in enumerate(stdoutLines):
if re.match(r'data:image/\w+;base64', line):
image_url = get_image_url_from_base64(
image_url = await get_image_url_from_base64(
request,
line,
metadata,
@@ -4558,7 +4559,7 @@ async def streaming_chat_response_handler(response, ctx):
resultLines = result.split('\n')
for idx, line in enumerate(resultLines):
if re.match(r'data:image/\w+;base64', line):
image_url = get_image_url_from_base64(
image_url = await get_image_url_from_base64(
request,
line,
metadata,
@@ -4623,7 +4624,7 @@ async def streaming_chat_response_handler(response, ctx):
if item.get('status') == 'in_progress':
item['status'] = 'completed'
title = Chats.get_chat_title_by_id(metadata['chat_id'])
title = await Chats.get_chat_title_by_id(metadata['chat_id'])
data = {
'done': True,
'content': serialize_output(output),
@@ -4634,7 +4635,7 @@ async def streaming_chat_response_handler(response, ctx):
if not ENABLE_REALTIME_CHAT_SAVE:
# Save message in the database
Chats.upsert_message_to_chat_by_id_and_message_id(
await Chats.upsert_message_to_chat_by_id_and_message_id(
metadata['chat_id'],
metadata['message_id'],
{
@@ -4645,21 +4646,21 @@ async def streaming_chat_response_handler(response, ctx):
},
)
elif usage:
Chats.upsert_message_to_chat_by_id_and_message_id(
await Chats.upsert_message_to_chat_by_id_and_message_id(
metadata['chat_id'],
metadata['message_id'],
{'done': True, 'usage': usage},
)
else:
Chats.upsert_message_to_chat_by_id_and_message_id(
await Chats.upsert_message_to_chat_by_id_and_message_id(
metadata['chat_id'],
metadata['message_id'],
{'done': True},
)
# Send a webhook notification if the user is not active
if request.app.state.config.ENABLE_USER_WEBHOOKS and not Users.is_user_active(user.id):
webhook_url = Users.get_user_webhook_url_by_id(user.id)
if request.app.state.config.ENABLE_USER_WEBHOOKS and not await Users.is_user_active(user.id):
webhook_url = await Users.get_user_webhook_url_by_id(user.id)
if webhook_url:
await post_webhook(
request.app.state.WEBUI_NAME,
@@ -4687,7 +4688,7 @@ async def streaming_chat_response_handler(response, ctx):
if not ENABLE_REALTIME_CHAT_SAVE:
# Save message in the database
Chats.upsert_message_to_chat_by_id_and_message_id(
await Chats.upsert_message_to_chat_by_id_and_message_id(
metadata['chat_id'],
metadata['message_id'],
{
@@ -4697,7 +4698,7 @@ async def streaming_chat_response_handler(response, ctx):
},
)
else:
Chats.upsert_message_to_chat_by_id_and_message_id(
await Chats.upsert_message_to_chat_by_id_and_message_id(
metadata['chat_id'],
metadata['message_id'],
{'done': True},
+16 -16
View File
@@ -130,13 +130,13 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
]
models = models + arena_models
global_action_ids = {function.id for function in Functions.get_global_action_functions()}
enabled_action_ids = {function.id for function in Functions.get_functions_by_type('action', active_only=True)}
global_action_ids = {function.id for function in await Functions.get_global_action_functions()}
enabled_action_ids = {function.id for function in await Functions.get_functions_by_type('action', active_only=True)}
global_filter_ids = {function.id for function in Functions.get_global_filter_functions()}
enabled_filter_ids = {function.id for function in Functions.get_functions_by_type('filter', active_only=True)}
global_filter_ids = {function.id for function in await Functions.get_global_filter_functions()}
enabled_filter_ids = {function.id for function in await Functions.get_functions_by_type('filter', active_only=True)}
custom_models = Models.get_all_models()
custom_models = await Models.get_all_models()
# Single O(1) lookup: Ollama base names first, then exact IDs (exact wins).
base_model_lookup = {}
@@ -278,14 +278,14 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
all_function_ids.update(global_action_ids)
all_function_ids.update(global_filter_ids)
functions_by_id = {f.id: f for f in Functions.get_functions_by_ids(list(all_function_ids))}
functions_by_id = {f.id: f for f in await Functions.get_functions_by_ids(list(all_function_ids))}
# Pre-warm the function module cache once per unique function ID.
# This ensures each function's DB freshness check runs exactly once,
# not once per (model × function) pair.
for function_id in all_function_ids:
try:
get_function_module_from_cache(request, function_id)
await get_function_module_from_cache(request, function_id)
except Exception as e:
log.info(f'Failed to load function module for {function_id}: {e}')
@@ -312,7 +312,7 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
# Batch-fetch all function valves in one query to avoid N+1 DB hits
# inside get_action_priority (previously called per action × per model).
all_function_valves = Functions.get_function_valves_by_ids(list(all_function_ids))
all_function_valves = await Functions.get_function_valves_by_ids(list(all_function_ids))
def get_action_priority(action_id):
try:
@@ -377,11 +377,11 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
return models
def check_model_access(user, model, db=None):
async def check_model_access(user, model, db=None):
if model.get('arena'):
meta = model.get('info', {}).get('meta', {})
access_grants = meta.get('access_grants', [])
if not has_access(
if not await has_access(
user.id,
permission='read',
access_grants=access_grants,
@@ -389,12 +389,12 @@ def check_model_access(user, model, db=None):
):
raise Exception('Model not found')
else:
model_info = Models.get_model_by_id(model.get('id'), db=db)
model_info = await Models.get_model_by_id(model.get('id'), db=db)
if not model_info:
raise Exception('Model not found')
elif not (
user.id == model_info.user_id
or AccessGrants.has_access(
or await AccessGrants.has_access(
user_id=user.id,
resource_type='model',
resource_id=model_info.id,
@@ -405,7 +405,7 @@ def check_model_access(user, model, db=None):
raise Exception('Model not found')
def get_filtered_models(models, user, db=None):
async def get_filtered_models(models, user, db=None):
# Filter out models that the user does not have access to
if (
user.role == 'user' or (user.role == 'admin' and not BYPASS_ADMIN_ACCESS_CONTROL)
@@ -418,10 +418,10 @@ def get_filtered_models(models, user, db=None):
if info:
model_infos[model['id']] = info
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id, db=db)}
user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)}
# Batch-fetch accessible resource IDs in a single query instead of N has_access calls
accessible_model_ids = AccessGrants.get_accessible_resource_ids(
accessible_model_ids = await AccessGrants.get_accessible_resource_ids(
user_id=user.id,
resource_type='model',
resource_ids=list(model_infos.keys()),
@@ -435,7 +435,7 @@ def get_filtered_models(models, user, db=None):
if model.get('arena'):
meta = model.get('info', {}).get('meta', {})
access_grants = meta.get('access_grants', [])
if has_access(
if await has_access(
user.id,
permission='read',
access_grants=access_grants,
+44 -44
View File
@@ -700,7 +700,7 @@ class OAuthClientManager:
"""
try:
# Get the OAuth session
session = OAuthSessions.get_session_by_provider_and_user_id(client_id, user_id)
session = await OAuthSessions.get_session_by_provider_and_user_id(client_id, user_id)
if not session:
log.warning(f'No OAuth session found for user {user_id}, client_id {client_id}')
return None
@@ -714,7 +714,7 @@ class OAuthClientManager:
log.warning(
f'Token refresh failed for user {user_id}, client_id {session.provider}, deleting session {session.id}'
)
OAuthSessions.delete_session_by_id(session.id)
await OAuthSessions.delete_session_by_id(session.id)
return None
return session.token
@@ -738,7 +738,7 @@ class OAuthClientManager:
if refreshed_token:
# Update the session with new token data
session = OAuthSessions.update_session_by_id(session.id, refreshed_token)
session = await OAuthSessions.update_session_by_id(session.id, refreshed_token)
log.info(f'Successfully refreshed token for session {session.id}')
return session.token
else:
@@ -884,12 +884,12 @@ class OAuthClientManager:
token['expires_at'] = datetime.now().timestamp() + token['expires_in']
# Clean up any existing sessions for this user/client_id first
sessions = OAuthSessions.get_sessions_by_user_id(user_id)
sessions = await OAuthSessions.get_sessions_by_user_id(user_id)
for session in sessions:
if session.provider == client_id:
OAuthSessions.delete_session_by_id(session.id)
await OAuthSessions.delete_session_by_id(session.id)
session = OAuthSessions.create_session(
session = await OAuthSessions.create_session(
user_id=user_id,
provider=client_id,
token=token,
@@ -963,7 +963,7 @@ class OAuthManager:
"""
try:
# Get the OAuth session
session = OAuthSessions.get_session_by_id_and_user_id(session_id, user_id)
session = await OAuthSessions.get_session_by_id_and_user_id(session_id, user_id)
if not session:
log.warning(f'No OAuth session found for user {user_id}, session {session_id}')
return None
@@ -977,7 +977,7 @@ class OAuthManager:
log.warning(
f'Token refresh failed for user {user_id}, provider {session.provider}, deleting session {session.id}'
)
OAuthSessions.delete_session_by_id(session.id)
await OAuthSessions.delete_session_by_id(session.id)
return None
return session.token
@@ -1002,7 +1002,7 @@ class OAuthManager:
if refreshed_token:
# Update the session with new token data
session = OAuthSessions.update_session_by_id(session.id, refreshed_token)
session = await OAuthSessions.update_session_by_id(session.id, refreshed_token)
log.info(f'Successfully refreshed token for session {session.id}')
return session.token
else:
@@ -1102,8 +1102,8 @@ class OAuthManager:
log.error(f'Exception during token refresh for provider {provider}: {e}')
return None
def get_user_role(self, user, user_data):
user_count = Users.get_num_users()
async def get_user_role(self, user, user_data):
user_count = await Users.get_num_users()
if user and user_count == 1:
# If the user is the only user, assign the role "admin" - actually repairs role for single user on login
log.debug('Assigning the only user the admin role')
@@ -1188,7 +1188,7 @@ class OAuthManager:
return role
def update_user_groups(self, user, user_data, default_permissions, db=None):
async def update_user_groups(self, user, user_data, default_permissions, db=None):
log.debug('Running OAUTH Group management')
oauth_claim = auth_manager_config.OAUTH_GROUPS_CLAIM
@@ -1217,8 +1217,8 @@ class OAuthManager:
else:
user_oauth_groups = []
user_current_groups: list[GroupModel] = Groups.get_groups_by_member_id(user.id, db=db)
all_available_groups: list[GroupModel] = Groups.get_all_groups(db=db)
user_current_groups: list[GroupModel] = await Groups.get_groups_by_member_id(user.id, db=db)
all_available_groups: list[GroupModel] = await Groups.get_all_groups(db=db)
# Create groups if they don't exist and creation is enabled
if auth_manager_config.ENABLE_OAUTH_GROUP_CREATION:
@@ -1226,7 +1226,7 @@ class OAuthManager:
all_group_names = {g.name for g in all_available_groups}
groups_created = False
# Determine creator ID: Prefer admin, fallback to current user if no admin exists
admin_user = Users.get_super_admin_user()
admin_user = await Users.get_super_admin_user()
creator_id = admin_user.id if admin_user else user.id
log.debug(f'Using creator ID {creator_id} for potential group creation.')
@@ -1241,7 +1241,7 @@ class OAuthManager:
data={'config': {'share': auth_manager_config.OAUTH_GROUP_DEFAULT_SHARE}},
)
# Use determined creator ID (admin or fallback to current user)
created_group = Groups.insert_new_group(creator_id, new_group_form, db=db)
created_group = await Groups.insert_new_group(creator_id, new_group_form, db=db)
if created_group:
log.info(
f"Successfully created group '{group_name}' with ID {created_group.id} using creator ID {creator_id}"
@@ -1256,7 +1256,7 @@ class OAuthManager:
# Refresh the list of all available groups if any were created
if groups_created:
all_available_groups = Groups.get_all_groups(db=db)
all_available_groups = await Groups.get_all_groups(db=db)
log.debug('Refreshed list of all available groups after creation.')
log.debug(f'Oauth Groups claim: {oauth_claim}')
@@ -1273,14 +1273,14 @@ class OAuthManager:
):
# Remove group from user
log.debug(f'Removing user from group {group_model.name} as it is no longer in their oauth groups')
Groups.remove_users_from_group(group_model.id, [user.id], db=db)
await Groups.remove_users_from_group(group_model.id, [user.id], db=db)
# In case a group is created, but perms are never assigned to the group by hitting "save"
group_permissions = group_model.permissions
if not group_permissions:
group_permissions = default_permissions
Groups.update_group_by_id(
await Groups.update_group_by_id(
id=group_model.id,
form_data=GroupUpdateForm(
name=group_model.name,
@@ -1302,14 +1302,14 @@ class OAuthManager:
# Add user to group
log.debug(f'Adding user to group {group_model.name} as it was found in their oauth groups')
Groups.add_users_to_group(group_model.id, [user.id], db=db)
await Groups.add_users_to_group(group_model.id, [user.id], db=db)
# In case a group is created, but perms are never assigned to the group by hitting "save"
group_permissions = group_model.permissions
if not group_permissions:
group_permissions = default_permissions
Groups.update_group_by_id(
await Groups.update_group_by_id(
id=group_model.id,
form_data=GroupUpdateForm(
name=group_model.name,
@@ -1490,20 +1490,20 @@ class OAuthManager:
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
# Check if the user exists
user = Users.get_user_by_oauth_sub(provider, sub, db=db)
user = await Users.get_user_by_oauth_sub(provider, sub, db=db)
if not user:
# If the user does not exist, check if merging is enabled
if auth_manager_config.OAUTH_MERGE_ACCOUNTS_BY_EMAIL:
# Check if the user exists by email
user = Users.get_user_by_email(email, db=db)
user = await Users.get_user_by_email(email, db=db)
if user:
# Update the user with the new oauth sub
Users.update_user_oauth_by_id(user.id, provider, sub, db=db)
await Users.update_user_oauth_by_id(user.id, provider, sub, db=db)
if user:
determined_role = self.get_user_role(user, user_data)
determined_role = await self.get_user_role(user, user_data)
if user.role != determined_role:
Users.update_user_role_by_id(user.id, determined_role, db=db)
await Users.update_user_role_by_id(user.id, determined_role, db=db)
# Update the user object in memory as well,
# to avoid problems with the ENABLE_OAUTH_GROUP_MANAGEMENT check below
user.role = determined_role
@@ -1513,7 +1513,7 @@ class OAuthManager:
if username_claim:
new_name = user_data.get(username_claim)
if new_name and new_name != user.name:
Users.update_user_by_id(user.id, {'name': new_name}, db=db)
await Users.update_user_by_id(user.id, {'name': new_name}, db=db)
user.name = new_name
log.debug(f'Updated name for user {user.email}')
@@ -1522,13 +1522,13 @@ class OAuthManager:
if email_claim:
new_email = user_data.get(email_claim)
if new_email and new_email.lower() != user.email.lower():
existing_user = Users.get_user_by_email(new_email, db=db)
existing_user = await Users.get_user_by_email(new_email, db=db)
if existing_user:
log.error(
f'Cannot update email to {new_email} for user {user.id} because it is already taken.'
)
else:
Auths.update_email_by_id(user.id, new_email.lower(), db=db)
await Auths.update_email_by_id(user.id, new_email.lower(), db=db)
user.email = new_email.lower()
log.debug(f'Updated email for user {user.id}')
@@ -1544,13 +1544,13 @@ class OAuthManager:
new_picture_url, token.get('access_token')
)
if processed_picture_url != user.profile_image_url:
Users.update_user_profile_image_url_by_id(user.id, processed_picture_url, db=db)
await Users.update_user_profile_image_url_by_id(user.id, processed_picture_url, db=db)
log.debug(f'Updated profile picture for user {user.email}')
else:
# If the user does not exist, check if signups are enabled
if auth_manager_config.ENABLE_OAUTH_SIGNUP:
# Check if an existing user with the same email already exists
existing_user = Users.get_user_by_email(email, db=db)
existing_user = await Users.get_user_by_email(email, db=db)
if existing_user:
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
@@ -1570,12 +1570,12 @@ class OAuthManager:
log.warning('Username claim is missing, using email as name')
name = email
user = Auths.insert_new_auth(
user = await Auths.insert_new_auth(
email=email,
password=get_password_hash(str(uuid.uuid4())), # Random password, not used
name=name,
profile_image_url=picture_url,
role=self.get_user_role(None, user_data),
role=await self.get_user_role(None, user_data),
oauth=oauth_data,
db=db,
)
@@ -1586,9 +1586,9 @@ class OAuthManager:
# Atomically check if this is the only user *after* the
# insert to avoid TOCTOU race on first-user registration.
# Matches signup_handler pattern.
if Users.get_num_users(db=db) == 1:
Users.update_user_role_by_id(user.id, 'admin', db=db)
user = Users.get_user_by_id(user.id, db=db)
if await Users.get_num_users(db=db) == 1:
await Users.update_user_role_by_id(user.id, 'admin', db=db)
user = await Users.get_user_by_id(user.id, db=db)
if auth_manager_config.WEBHOOK_URL:
await post_webhook(
@@ -1602,7 +1602,7 @@ class OAuthManager:
},
)
apply_default_group_assignment(request.app.state.config.DEFAULT_GROUP_ID, user.id, db=db)
await apply_default_group_assignment(request.app.state.config.DEFAULT_GROUP_ID, user.id, db=db)
else:
raise HTTPException(
@@ -1615,7 +1615,7 @@ class OAuthManager:
expires_delta=parse_duration(auth_manager_config.JWT_EXPIRES_IN),
)
if auth_manager_config.ENABLE_OAUTH_GROUP_MANAGEMENT:
self.update_user_groups(
await self.update_user_groups(
user=user,
user_data=user_data,
default_permissions=request.app.state.config.USER_PERMISSIONS,
@@ -1675,7 +1675,7 @@ class OAuthManager:
# Enforce max concurrent sessions per user/provider to prevent
# unbounded growth while allowing multi-device usage
sessions = OAuthSessions.get_sessions_by_user_id(user.id, db=db)
sessions = await OAuthSessions.get_sessions_by_user_id(user.id, db=db)
provider_sessions = sorted(
[session for session in sessions if session.provider == provider],
key=lambda session: session.created_at,
@@ -1684,9 +1684,9 @@ class OAuthManager:
# Keep the newest sessions up to the limit, prune the rest
if len(provider_sessions) >= OAUTH_MAX_SESSIONS_PER_USER:
for old_session in provider_sessions[OAUTH_MAX_SESSIONS_PER_USER - 1 :]:
OAuthSessions.delete_session_by_id(old_session.id, db=db)
await OAuthSessions.delete_session_by_id(old_session.id, db=db)
session = OAuthSessions.create_session(
session = await OAuthSessions.create_session(
user_id=user.id,
provider=provider,
token=token,
@@ -1847,7 +1847,7 @@ class OAuthManager:
# 8. Identify users to log out
users_to_logout = []
if sub:
user = Users.get_user_by_oauth_sub(matched_provider, sub, db=db)
user = await Users.get_user_by_oauth_sub(matched_provider, sub, db=db)
if user:
users_to_logout.append(user)
@@ -1868,9 +1868,9 @@ class OAuthManager:
revoked_count = 0
for user in users_to_logout:
sessions = OAuthSessions.get_sessions_by_user_id(user.id, db=db)
sessions = await OAuthSessions.get_sessions_by_user_id(user.id, db=db)
for oauth_session in sessions:
OAuthSessions.delete_session_by_id(oauth_session.id, db=db)
await OAuthSessions.delete_session_by_id(oauth_session.id, db=db)
if redis:
revocation_key = f'{REDIS_KEY_PREFIX}:auth:user:{user.id}:revoked_at'
+20 -20
View File
@@ -199,16 +199,16 @@ def replace_imports(content):
# May the intent of the one who wrote it survive every
# import and transformation, as a deed survives the generations.
def load_tool_module_by_id(tool_id, content=None):
async def load_tool_module_by_id(tool_id, content=None):
if content is None:
tool = Tools.get_tool_by_id(tool_id)
tool = await Tools.get_tool_by_id(tool_id)
if not tool:
raise Exception(f'Toolkit not found: {tool_id}')
content = tool.content
content = replace_imports(content)
Tools.update_tool_by_id(tool_id, {'content': content})
await Tools.update_tool_by_id(tool_id, {'content': content})
else:
frontmatter = extract_frontmatter(content)
# Install required packages found within the frontmatter
@@ -245,15 +245,15 @@ def load_tool_module_by_id(tool_id, content=None):
os.unlink(temp_file.name)
def load_function_module_by_id(function_id: str, content: str | None = None):
async def load_function_module_by_id(function_id: str, content: str | None = None):
if content is None:
function = Functions.get_function_by_id(function_id)
function = await Functions.get_function_by_id(function_id)
if not function:
raise Exception(f'Function not found: {function_id}')
content = function.content
content = replace_imports(content)
Functions.update_function_by_id(function_id, {'content': content})
await Functions.update_function_by_id(function_id, {'content': content})
else:
frontmatter = extract_frontmatter(content)
install_frontmatter_requirements(frontmatter.get('requirements', ''))
@@ -290,16 +290,16 @@ def load_function_module_by_id(function_id: str, content: str | None = None):
# Cleanup by removing the module in case of error
del sys.modules[module_name]
Functions.update_function_by_id(function_id, {'is_active': False})
await Functions.update_function_by_id(function_id, {'is_active': False})
raise e
finally:
os.unlink(temp_file.name)
def get_tool_module_from_cache(request, tool_id, load_from_db=True):
async def get_tool_module_from_cache(request, tool_id, load_from_db=True):
if load_from_db:
# Always load from the database by default
tool = Tools.get_tool_by_id(tool_id)
tool = await Tools.get_tool_by_id(tool_id)
if not tool:
raise Exception(f'Tool not found: {tool_id}')
content = tool.content
@@ -308,7 +308,7 @@ def get_tool_module_from_cache(request, tool_id, load_from_db=True):
if new_content != content:
content = new_content
# Update the tool content in the database
Tools.update_tool_by_id(tool_id, {'content': content})
await Tools.update_tool_by_id(tool_id, {'content': content})
if (hasattr(request.app.state, 'TOOL_CONTENTS') and tool_id in request.app.state.TOOL_CONTENTS) and (
hasattr(request.app.state, 'TOOLS') and tool_id in request.app.state.TOOLS
@@ -316,12 +316,12 @@ def get_tool_module_from_cache(request, tool_id, load_from_db=True):
if request.app.state.TOOL_CONTENTS[tool_id] == content:
return request.app.state.TOOLS[tool_id], None
tool_module, frontmatter = load_tool_module_by_id(tool_id, content)
tool_module, frontmatter = await load_tool_module_by_id(tool_id, content)
else:
if hasattr(request.app.state, 'TOOLS') and tool_id in request.app.state.TOOLS:
return request.app.state.TOOLS[tool_id], None
tool_module, frontmatter = load_tool_module_by_id(tool_id)
tool_module, frontmatter = await load_tool_module_by_id(tool_id)
if not hasattr(request.app.state, 'TOOLS'):
request.app.state.TOOLS = {}
@@ -335,13 +335,13 @@ def get_tool_module_from_cache(request, tool_id, load_from_db=True):
return tool_module, frontmatter
def get_function_module_from_cache(request, function_id, load_from_db=True):
async def get_function_module_from_cache(request, function_id, load_from_db=True):
if load_from_db:
# Always load from the database by default
# This is useful for hooks like "inlet" or "outlet" where the content might change
# and we want to ensure the latest content is used.
function = Functions.get_function_by_id(function_id)
function = await Functions.get_function_by_id(function_id)
if not function:
raise Exception(f'Function not found: {function_id}')
content = function.content
@@ -350,7 +350,7 @@ def get_function_module_from_cache(request, function_id, load_from_db=True):
if new_content != content:
content = new_content
# Update the function content in the database
Functions.update_function_by_id(function_id, {'content': content})
await Functions.update_function_by_id(function_id, {'content': content})
if (
hasattr(request.app.state, 'FUNCTION_CONTENTS') and function_id in request.app.state.FUNCTION_CONTENTS
@@ -358,7 +358,7 @@ def get_function_module_from_cache(request, function_id, load_from_db=True):
if request.app.state.FUNCTION_CONTENTS[function_id] == content:
return request.app.state.FUNCTIONS[function_id], None, None
function_module, function_type, frontmatter = load_function_module_by_id(function_id, content)
function_module, function_type, frontmatter = await load_function_module_by_id(function_id, content)
else:
# Load from cache (e.g. "stream" hook)
# This is useful for performance reasons
@@ -366,7 +366,7 @@ def get_function_module_from_cache(request, function_id, load_from_db=True):
if hasattr(request.app.state, 'FUNCTIONS') and function_id in request.app.state.FUNCTIONS:
return request.app.state.FUNCTIONS[function_id], None, None
function_module, function_type, frontmatter = load_function_module_by_id(function_id)
function_module, function_type, frontmatter = await load_function_module_by_id(function_id)
if not hasattr(request.app.state, 'FUNCTIONS'):
request.app.state.FUNCTIONS = {}
@@ -404,7 +404,7 @@ def install_frontmatter_requirements(requirements: str):
log.info('No requirements found in frontmatter.')
def install_tool_and_function_dependencies():
async def install_tool_and_function_dependencies():
"""
Install all dependencies for all admin tools and active functions.
@@ -412,8 +412,8 @@ def install_tool_and_function_dependencies():
and then installing them using pip. Duplicates or similar version specifications are
handled by pip as much as possible.
"""
function_list = Functions.get_functions(active_only=True)
tool_list = Tools.get_tools()
function_list = await Functions.get_functions(active_only=True)
tool_list = await Tools.get_tools()
all_dependencies = ''
try:
+1 -1
View File
@@ -38,7 +38,7 @@ class SentinelRedisProxy:
def _master(self):
return self._sentinel.master_for(self._service, **self._kw)
def __getattr__(self, item):
async def __getattr__(self, item):
master = self._master()
orig_attr = getattr(master, item)
@@ -124,16 +124,16 @@ def setup_metrics(app: FastAPI, resource: Resource) -> None:
unit='ms',
)
def observe_active_users(
async def observe_active_users(
options: metrics.CallbackOptions,
) -> Sequence[metrics.Observation]:
return [
metrics.Observation(
value=Users.get_active_user_count(),
value=await Users.get_active_user_count(),
)
]
def observe_total_registered_users(
async def observe_total_registered_users(
options: metrics.CallbackOptions,
) -> Sequence[metrics.Observation]:
# IMPORTANT: Use get_num_users() for efficient COUNT(*) query.
@@ -141,7 +141,7 @@ def setup_metrics(app: FastAPI, resource: Resource) -> None:
# causing connection pool exhaustion on high-latency databases (e.g., Aurora).
return [
metrics.Observation(
value=Users.get_num_users() or 0,
value=await Users.get_num_users() or 0,
)
]
@@ -159,10 +159,10 @@ def setup_metrics(app: FastAPI, resource: Resource) -> None:
callbacks=[observe_active_users],
)
def observe_users_active_today(
async def observe_users_active_today(
options: metrics.CallbackOptions,
) -> Sequence[metrics.Observation]:
return [metrics.Observation(value=Users.get_num_users_active_today())]
return [metrics.Observation(value=await Users.get_num_users_active_today())]
meter.create_observable_gauge(
name='webui.users.active.today',
+31 -31
View File
@@ -101,7 +101,7 @@ log = logging.getLogger(__name__)
# Let no function be called without need, and let what
# it yields justify the cost of running it.
def get_async_tool_function_and_apply_extra_params(function: Callable, extra_params: dict) -> Callable[..., Awaitable]:
async def get_async_tool_function_and_apply_extra_params(function: Callable, extra_params: dict) -> Callable[..., Awaitable]:
sig = inspect.signature(function)
extra_params = {k: v for k, v in extra_params.items() if k in sig.parameters}
partial_func = partial(function, **extra_params)
@@ -138,13 +138,13 @@ def get_async_tool_function_and_apply_extra_params(function: Callable, extra_par
return new_function
def get_updated_tool_function(function: Callable, extra_params: dict):
async def get_updated_tool_function(function: Callable, extra_params: dict):
# Get the original function and merge updated params
__function__ = getattr(function, '__function__', None)
__extra_params__ = getattr(function, '__extra_params__', None)
if __function__ is not None and __extra_params__ is not None:
return get_async_tool_function_and_apply_extra_params(
return await get_async_tool_function_and_apply_extra_params(
__function__,
{**__extra_params__, **extra_params},
)
@@ -160,16 +160,16 @@ async def get_tools(request: Request, tool_ids: list[str], user: UserModel, extr
tools_dict = {}
# Get user's group memberships for access control checks
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id)}
user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id)}
for tool_id in tool_ids:
tool = Tools.get_tool_by_id(tool_id)
tool = await Tools.get_tool_by_id(tool_id)
if tool:
# Check access control for local tools
if (
not (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL)
and tool.user_id != user.id
and not AccessGrants.has_access(
and not await AccessGrants.has_access(
user_id=user.id,
resource_type='tool',
resource_id=tool.id,
@@ -182,7 +182,7 @@ async def get_tools(request: Request, tool_ids: list[str], user: UserModel, extr
module = request.app.state.TOOLS.get(tool_id, None)
if module is None:
module, _ = load_tool_module_by_id(tool_id)
module, _ = await load_tool_module_by_id(tool_id)
request.app.state.TOOLS[tool_id] = module
__user__ = {
@@ -191,11 +191,11 @@ async def get_tools(request: Request, tool_ids: list[str], user: UserModel, extr
# Set valves for the tool
if hasattr(module, 'valves') and hasattr(module, 'Valves'):
valves = Tools.get_tool_valves_by_id(tool_id) or {}
valves = await Tools.get_tool_valves_by_id(tool_id) or {}
module.valves = module.Valves(**valves)
if hasattr(module, 'UserValves'):
__user__['valves'] = module.UserValves( # type: ignore
**Tools.get_user_valves_by_id_and_user_id(tool_id, user.id)
**await Tools.get_user_valves_by_id_and_user_id(tool_id, user.id)
)
for spec in tool.specs:
@@ -213,7 +213,7 @@ async def get_tools(request: Request, tool_ids: list[str], user: UserModel, extr
# convert to function that takes only model params and inserts custom params
function_name = spec['name']
tool_function = getattr(module, function_name)
callable = get_async_tool_function_and_apply_extra_params(
callable = await get_async_tool_function_and_apply_extra_params(
tool_function,
{
**extra_params,
@@ -285,7 +285,7 @@ async def get_tools(request: Request, tool_ids: list[str], user: UserModel, extr
tool_server_connection = connections[tool_server_idx]
# Check access control for tool server
if not has_connection_access(user, tool_server_connection, user_group_ids):
if not await has_connection_access(user, tool_server_connection, user_group_ids):
log.warning(f'Access denied to tool server {server_id} for user {user.id}')
continue
@@ -339,7 +339,7 @@ async def get_tools(request: Request, tool_ids: list[str], user: UserModel, extr
if metadata and metadata.get('message_id'):
headers[FORWARD_SESSION_INFO_HEADER_MESSAGE_ID] = metadata.get('message_id')
def make_tool_function(function_name, tool_server_data, headers):
async def make_tool_function(function_name, tool_server_data, headers):
async def tool_function(**kwargs):
return await execute_tool_server(
url=tool_server_data['url'],
@@ -352,9 +352,9 @@ async def get_tools(request: Request, tool_ids: list[str], user: UserModel, extr
return tool_function
tool_function = make_tool_function(function_name, tool_server_data, headers)
tool_function = await make_tool_function(function_name, tool_server_data, headers)
callable = get_async_tool_function_and_apply_extra_params(
callable = await get_async_tool_function_and_apply_extra_params(
tool_function,
{},
)
@@ -381,7 +381,7 @@ async def get_tools(request: Request, tool_ids: list[str], user: UserModel, extr
return tools_dict
def get_builtin_tools(
async def get_builtin_tools(
request: Request, extra_params: dict, features: dict = None, model: dict = None
) -> dict[str, dict]:
"""
@@ -406,10 +406,10 @@ def get_builtin_tools(
# Helper to check user-level feature permission (admins always pass)
user = extra_params.get('__user__', {})
def has_user_permission(feature_key: str) -> bool:
async def has_user_permission(feature_key: str) -> bool:
if user.get('role') == 'admin':
return True
return has_permission(
return await has_permission(
user.get('id', ''),
f'features.{feature_key}',
request.app.state.config.USER_PERMISSIONS,
@@ -461,7 +461,7 @@ def get_builtin_tools(
if (
is_builtin_tool_enabled('memory')
and (features.get('memory') or get_model_capability('memory', False))
and has_user_permission('memories')
and await has_user_permission('memories')
):
builtin_functions.extend(
[
@@ -479,7 +479,7 @@ def get_builtin_tools(
and getattr(request.app.state.config, 'ENABLE_WEB_SEARCH', False)
and get_model_capability('web_search')
and features.get('web_search')
and has_user_permission('web_search')
and await has_user_permission('web_search')
):
builtin_functions.extend([search_web, fetch_url])
@@ -489,7 +489,7 @@ def get_builtin_tools(
and getattr(request.app.state.config, 'ENABLE_IMAGE_GENERATION', False)
and get_model_capability('image_generation')
and features.get('image_generation')
and has_user_permission('image_generation')
and await has_user_permission('image_generation')
):
builtin_functions.append(generate_image)
if (
@@ -497,7 +497,7 @@ def get_builtin_tools(
and getattr(request.app.state.config, 'ENABLE_IMAGE_EDIT', False)
and get_model_capability('image_generation')
and features.get('image_generation')
and has_user_permission('image_generation')
and await has_user_permission('image_generation')
):
builtin_functions.append(edit_image)
@@ -507,7 +507,7 @@ def get_builtin_tools(
and getattr(request.app.state.config, 'ENABLE_CODE_INTERPRETER', True)
and get_model_capability('code_interpreter')
and features.get('code_interpreter')
and has_user_permission('code_interpreter')
and await has_user_permission('code_interpreter')
):
builtin_functions.append(execute_code)
@@ -515,7 +515,7 @@ def get_builtin_tools(
if (
is_builtin_tool_enabled('notes')
and getattr(request.app.state.config, 'ENABLE_NOTES', False)
and has_user_permission('notes')
and await has_user_permission('notes')
):
builtin_functions.extend([search_notes, view_note, write_note, replace_note_content])
@@ -523,7 +523,7 @@ def get_builtin_tools(
if (
is_builtin_tool_enabled('channels')
and getattr(request.app.state.config, 'ENABLE_CHANNELS', False)
and has_user_permission('channels')
and await has_user_permission('channels')
):
builtin_functions.extend(
[
@@ -543,11 +543,11 @@ def get_builtin_tools(
builtin_functions.append(tasks)
# Automation tools - create and manage scheduled automations from chat
if is_builtin_tool_enabled('automations') and has_user_permission('automations'):
if is_builtin_tool_enabled('automations') and await has_user_permission('automations'):
builtin_functions.extend([create_automation, update_automation, list_automations, toggle_automation, delete_automation])
for func in builtin_functions:
callable = get_async_tool_function_and_apply_extra_params(
callable = await get_async_tool_function_and_apply_extra_params(
func,
{
'__request__': request,
@@ -1024,8 +1024,8 @@ async def get_terminal_tools(
log.warning(f'Terminal server not found: {terminal_id}')
return {}
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id)}
if not has_connection_access(user, connection, user_group_ids):
user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id)}
if not await has_connection_access(user, connection, user_group_ids):
log.warning(f'Access denied to terminal {terminal_id} for user {user.id}')
return {}
@@ -1077,7 +1077,7 @@ async def get_terminal_tools(
tool_spec.get('description', '') + f'\n\nThe current working directory is: {terminal_cwd}'
)
def make_tool_function(fn_name, srv_data, hdrs, cks):
async def make_tool_function(fn_name, srv_data, hdrs, cks):
async def tool_function(**kwargs):
return await execute_tool_server(
url=srv_data['url'],
@@ -1090,8 +1090,8 @@ async def get_terminal_tools(
return tool_function
tool_function = make_tool_function(function_name, server_data, headers, cookies)
callable = get_async_tool_function_and_apply_extra_params(tool_function, {})
tool_function = await make_tool_function(function_name, server_data, headers, cookies)
callable = await get_async_tool_function_and_apply_extra_params(tool_function, {})
tools_dict[function_name] = {
'tool_id': f'terminal:{terminal_id}',