refac: async db
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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''
|
||||
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'')
|
||||
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
|
||||
|
||||
|
||||
@@ -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}')
|
||||
|
||||
@@ -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}')
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}',
|
||||
|
||||
Reference in New Issue
Block a user