refac
This commit is contained in:
@@ -2494,3 +2494,332 @@ async def tasks(
|
||||
except Exception as e:
|
||||
log.exception(f'tasks error: {e}')
|
||||
return json.dumps({'error': str(e)})
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AUTOMATION TOOLS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
async def create_automation(
|
||||
name: str,
|
||||
prompt: str,
|
||||
rrule: str,
|
||||
model_id: Optional[str] = None,
|
||||
__request__: Request = None,
|
||||
__user__: dict = None,
|
||||
__metadata__: dict = None,
|
||||
) -> str:
|
||||
"""
|
||||
Create a scheduled automation that runs a prompt on a recurring or one-time schedule.
|
||||
Use this when the user wants to schedule a task to run automatically.
|
||||
|
||||
The rrule parameter must be a valid iCalendar RRULE string. Common examples:
|
||||
- Every day at 9am: "DTSTART:20250101T090000\\nRRULE:FREQ=DAILY"
|
||||
- Every Monday at 8am: "DTSTART:20250106T080000\\nRRULE:FREQ=WEEKLY;BYDAY=MO"
|
||||
- Every hour: "RRULE:FREQ=HOURLY;INTERVAL=1"
|
||||
- Every 30 minutes: "RRULE:FREQ=MINUTELY;INTERVAL=30"
|
||||
- Once at a specific time: "DTSTART:20250415T140000\\nRRULE:FREQ=DAILY;COUNT=1"
|
||||
- First day of every month: "DTSTART:20250101T090000\\nRRULE:FREQ=MONTHLY;BYMONTHDAY=1"
|
||||
|
||||
The DTSTART time should reflect the desired execution time. Use COUNT=1 for one-time automations.
|
||||
|
||||
:param name: A short descriptive name for the automation
|
||||
:param prompt: The prompt/instructions to execute on each run
|
||||
:param rrule: An iCalendar RRULE string defining the schedule
|
||||
:param model_id: Optional model ID to use. Defaults to the current chat model if omitted.
|
||||
:return: JSON with the created automation details including id, next scheduled runs
|
||||
"""
|
||||
if __request__ is None:
|
||||
return json.dumps({'error': 'Request context not available'})
|
||||
|
||||
if not __user__:
|
||||
return json.dumps({'error': 'User context not available'})
|
||||
|
||||
try:
|
||||
from open_webui.models.automations import Automations, AutomationForm, AutomationData
|
||||
from open_webui.models.users import Users
|
||||
from open_webui.utils.automations import validate_rrule, next_run_ns, next_n_runs_ns
|
||||
|
||||
user_id = __user__.get('id')
|
||||
user = Users.get_user_by_id(user_id)
|
||||
if not user:
|
||||
return json.dumps({'error': 'User not found'})
|
||||
|
||||
# Default to current chat's model if not specified
|
||||
if not model_id:
|
||||
model_id = (__metadata__ or {}).get('model_id') or (__metadata__ or {}).get('model')
|
||||
if not model_id:
|
||||
return json.dumps({'error': 'model_id is required (could not detect current model)'})
|
||||
|
||||
# Validate the RRULE
|
||||
try:
|
||||
validate_rrule(rrule)
|
||||
except ValueError as e:
|
||||
return json.dumps({'error': f'Invalid schedule: {e}'})
|
||||
|
||||
tz = user.timezone
|
||||
form = AutomationForm(
|
||||
name=name,
|
||||
data=AutomationData(
|
||||
prompt=prompt,
|
||||
model_id=model_id,
|
||||
rrule=rrule,
|
||||
),
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
automation = Automations.insert(user_id, form, next_run_ns(rrule, tz=tz))
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
'status': 'success',
|
||||
'id': automation.id,
|
||||
'name': automation.name,
|
||||
'model_id': model_id,
|
||||
'is_active': automation.is_active,
|
||||
'next_runs': next_n_runs_ns(rrule, tz=tz),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(f'create_automation error: {e}')
|
||||
return json.dumps({'error': str(e)})
|
||||
|
||||
|
||||
async def update_automation(
|
||||
automation_id: str,
|
||||
name: Optional[str] = None,
|
||||
prompt: Optional[str] = None,
|
||||
rrule: Optional[str] = None,
|
||||
model_id: Optional[str] = None,
|
||||
__request__: Request = None,
|
||||
__user__: dict = None,
|
||||
) -> str:
|
||||
"""
|
||||
Update an existing automation. Only the provided fields are changed; omitted fields stay the same.
|
||||
|
||||
:param automation_id: The ID of the automation to update
|
||||
:param name: New name for the automation (optional)
|
||||
:param prompt: New prompt/instructions (optional)
|
||||
:param rrule: New iCalendar RRULE schedule string (optional). See create_automation for format examples.
|
||||
:param model_id: New model ID to use (optional)
|
||||
:return: JSON with the updated automation details
|
||||
"""
|
||||
if __request__ is None:
|
||||
return json.dumps({'error': 'Request context not available'})
|
||||
|
||||
if not __user__:
|
||||
return json.dumps({'error': 'User context not available'})
|
||||
|
||||
try:
|
||||
from open_webui.models.automations import Automations, AutomationForm, AutomationData
|
||||
from open_webui.models.users import Users
|
||||
from open_webui.utils.automations import validate_rrule, next_run_ns, next_n_runs_ns
|
||||
|
||||
user_id = __user__.get('id')
|
||||
user = Users.get_user_by_id(user_id)
|
||||
|
||||
automation = Automations.get_by_id(automation_id)
|
||||
if not automation:
|
||||
return json.dumps({'error': 'Automation not found'})
|
||||
if automation.user_id != user_id:
|
||||
return json.dumps({'error': 'Access denied'})
|
||||
|
||||
# Merge provided fields with existing values
|
||||
new_name = name if name is not None else automation.name
|
||||
new_prompt = prompt if prompt is not None else automation.data.get('prompt', '')
|
||||
new_model_id = model_id if model_id is not None else automation.data.get('model_id', '')
|
||||
new_rrule = rrule if rrule is not None else automation.data.get('rrule', '')
|
||||
|
||||
# Validate RRULE if changed
|
||||
if rrule is not None:
|
||||
try:
|
||||
validate_rrule(new_rrule)
|
||||
except ValueError as e:
|
||||
return json.dumps({'error': f'Invalid schedule: {e}'})
|
||||
|
||||
tz = user.timezone if user else None
|
||||
form = AutomationForm(
|
||||
name=new_name,
|
||||
data=AutomationData(
|
||||
prompt=new_prompt,
|
||||
model_id=new_model_id,
|
||||
rrule=new_rrule,
|
||||
),
|
||||
is_active=automation.is_active,
|
||||
)
|
||||
|
||||
updated = Automations.update(automation_id, form, next_run_ns(new_rrule, tz=tz))
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
'status': 'success',
|
||||
'id': updated.id,
|
||||
'name': updated.name,
|
||||
'model_id': new_model_id,
|
||||
'is_active': updated.is_active,
|
||||
'next_runs': next_n_runs_ns(new_rrule, tz=tz),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(f'update_automation error: {e}')
|
||||
return json.dumps({'error': str(e)})
|
||||
|
||||
|
||||
async def list_automations(
|
||||
status: Optional[str] = None,
|
||||
count: int = 10,
|
||||
__request__: Request = None,
|
||||
__user__: dict = None,
|
||||
) -> str:
|
||||
"""
|
||||
List the user's scheduled automations.
|
||||
|
||||
:param status: Filter by status: "active", "paused", or omit for all
|
||||
:param count: Maximum number of automations to return (default: 10)
|
||||
:return: JSON list of automations with id, name, prompt snippet, schedule, status, and next runs
|
||||
"""
|
||||
if __request__ is None:
|
||||
return json.dumps({'error': 'Request context not available'})
|
||||
|
||||
if not __user__:
|
||||
return json.dumps({'error': 'User context not available'})
|
||||
|
||||
try:
|
||||
from open_webui.models.automations import Automations
|
||||
from open_webui.models.users import Users
|
||||
from open_webui.utils.automations import next_n_runs_ns
|
||||
|
||||
user_id = __user__.get('id')
|
||||
user = Users.get_user_by_id(user_id)
|
||||
|
||||
result = Automations.search_automations(
|
||||
user_id=user_id,
|
||||
status=status,
|
||||
skip=0,
|
||||
limit=count,
|
||||
)
|
||||
|
||||
automations = []
|
||||
for item in result.items:
|
||||
rrule = item.data.get('rrule', '')
|
||||
prompt_text = item.data.get('prompt', '')
|
||||
snippet = prompt_text[:100] + ('...' if len(prompt_text) > 100 else '')
|
||||
|
||||
automations.append(
|
||||
{
|
||||
'id': item.id,
|
||||
'name': item.name,
|
||||
'prompt_snippet': snippet,
|
||||
'model_id': item.data.get('model_id', ''),
|
||||
'rrule': rrule,
|
||||
'is_active': item.is_active,
|
||||
'last_run_at': item.last_run_at,
|
||||
'next_runs': next_n_runs_ns(rrule, tz=user.timezone if user else None),
|
||||
}
|
||||
)
|
||||
|
||||
return json.dumps(
|
||||
{'automations': automations, 'total': result.total},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(f'list_automations error: {e}')
|
||||
return json.dumps({'error': str(e)})
|
||||
|
||||
|
||||
async def toggle_automation(
|
||||
automation_id: str,
|
||||
__request__: Request = None,
|
||||
__user__: dict = None,
|
||||
) -> str:
|
||||
"""
|
||||
Pause or resume a scheduled automation. If active, it will be paused. If paused, it will be resumed.
|
||||
|
||||
:param automation_id: The ID of the automation to toggle
|
||||
:return: JSON with the updated automation status
|
||||
"""
|
||||
if __request__ is None:
|
||||
return json.dumps({'error': 'Request context not available'})
|
||||
|
||||
if not __user__:
|
||||
return json.dumps({'error': 'User context not available'})
|
||||
|
||||
try:
|
||||
from open_webui.models.automations import Automations
|
||||
from open_webui.models.users import Users
|
||||
from open_webui.utils.automations import next_run_ns
|
||||
|
||||
user_id = __user__.get('id')
|
||||
user = Users.get_user_by_id(user_id)
|
||||
|
||||
automation = Automations.get_by_id(automation_id)
|
||||
if not automation:
|
||||
return json.dumps({'error': 'Automation not found'})
|
||||
if automation.user_id != user_id:
|
||||
return json.dumps({'error': 'Access denied'})
|
||||
|
||||
rrule = automation.data.get('rrule', '')
|
||||
toggled = Automations.toggle(
|
||||
automation_id,
|
||||
next_run_ns(rrule, tz=user.timezone if user else None),
|
||||
)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
'status': 'success',
|
||||
'id': toggled.id,
|
||||
'name': toggled.name,
|
||||
'is_active': toggled.is_active,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(f'toggle_automation error: {e}')
|
||||
return json.dumps({'error': str(e)})
|
||||
|
||||
|
||||
async def delete_automation(
|
||||
automation_id: str,
|
||||
__request__: Request = None,
|
||||
__user__: dict = None,
|
||||
) -> str:
|
||||
"""
|
||||
Delete a scheduled automation and all its run history.
|
||||
|
||||
:param automation_id: The ID of the automation to delete
|
||||
:return: JSON confirming the automation was deleted
|
||||
"""
|
||||
if __request__ is None:
|
||||
return json.dumps({'error': 'Request context not available'})
|
||||
|
||||
if not __user__:
|
||||
return json.dumps({'error': 'User context not available'})
|
||||
|
||||
try:
|
||||
from open_webui.models.automations import Automations, AutomationRuns
|
||||
|
||||
user_id = __user__.get('id')
|
||||
|
||||
automation = Automations.get_by_id(automation_id)
|
||||
if not automation:
|
||||
return json.dumps({'error': 'Automation not found'})
|
||||
if automation.user_id != user_id:
|
||||
return json.dumps({'error': 'Access denied'})
|
||||
|
||||
name = automation.name
|
||||
AutomationRuns.delete_by_automation(automation_id)
|
||||
Automations.delete(automation_id)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
'status': 'success',
|
||||
'message': f'Automation "{name}" deleted',
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(f'delete_automation error: {e}')
|
||||
return json.dumps({'error': str(e)})
|
||||
|
||||
@@ -86,9 +86,15 @@ from open_webui.tools.builtin import (
|
||||
view_knowledge_file,
|
||||
view_skill,
|
||||
tasks,
|
||||
create_automation,
|
||||
update_automation,
|
||||
list_automations,
|
||||
toggle_automation,
|
||||
delete_automation,
|
||||
)
|
||||
|
||||
import copy
|
||||
from open_webui.utils.access_control import has_permission
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -397,6 +403,18 @@ def get_builtin_tools(
|
||||
builtin_tools = model.get('info', {}).get('meta', {}).get('builtinTools', {})
|
||||
return builtin_tools.get(category, True)
|
||||
|
||||
# Helper to check user-level feature permission (admins always pass)
|
||||
user = extra_params.get('__user__', {})
|
||||
|
||||
def has_user_permission(feature_key: str) -> bool:
|
||||
if user.get('role') == 'admin':
|
||||
return True
|
||||
return has_permission(
|
||||
user.get('id', ''),
|
||||
f'features.{feature_key}',
|
||||
request.app.state.config.USER_PERMISSIONS,
|
||||
)
|
||||
|
||||
# Time utilities - available for date calculations
|
||||
if is_builtin_tool_enabled('time'):
|
||||
builtin_functions.extend([get_current_timestamp, calculate_timestamp])
|
||||
@@ -440,7 +458,11 @@ def get_builtin_tools(
|
||||
builtin_functions.extend([search_chats, view_chat])
|
||||
|
||||
# Add memory tools if builtin category enabled AND enabled for this chat
|
||||
if is_builtin_tool_enabled('memory') and (features.get('memory') or get_model_capability('memory', False)):
|
||||
if (
|
||||
is_builtin_tool_enabled('memory')
|
||||
and (features.get('memory') or get_model_capability('memory', False))
|
||||
and has_user_permission('memories')
|
||||
):
|
||||
builtin_functions.extend(
|
||||
[
|
||||
search_memories,
|
||||
@@ -457,6 +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')
|
||||
):
|
||||
builtin_functions.extend([search_web, fetch_url])
|
||||
|
||||
@@ -466,6 +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')
|
||||
):
|
||||
builtin_functions.append(generate_image)
|
||||
if (
|
||||
@@ -473,6 +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')
|
||||
):
|
||||
builtin_functions.append(edit_image)
|
||||
|
||||
@@ -482,15 +507,24 @@ 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')
|
||||
):
|
||||
builtin_functions.append(execute_code)
|
||||
|
||||
# Notes tools - search, view, create, and update user's notes (if builtin category enabled AND notes enabled globally)
|
||||
if is_builtin_tool_enabled('notes') and getattr(request.app.state.config, 'ENABLE_NOTES', False):
|
||||
# Notes tools - search, view, create, and update user's notes
|
||||
if (
|
||||
is_builtin_tool_enabled('notes')
|
||||
and getattr(request.app.state.config, 'ENABLE_NOTES', False)
|
||||
and has_user_permission('notes')
|
||||
):
|
||||
builtin_functions.extend([search_notes, view_note, write_note, replace_note_content])
|
||||
|
||||
# Channels tools - search channels and messages (if builtin category enabled AND channels enabled globally)
|
||||
if is_builtin_tool_enabled('channels') and getattr(request.app.state.config, 'ENABLE_CHANNELS', False):
|
||||
# Channels tools - search channels and messages
|
||||
if (
|
||||
is_builtin_tool_enabled('channels')
|
||||
and getattr(request.app.state.config, 'ENABLE_CHANNELS', False)
|
||||
and has_user_permission('channels')
|
||||
):
|
||||
builtin_functions.extend(
|
||||
[
|
||||
search_channels,
|
||||
@@ -508,6 +542,10 @@ def get_builtin_tools(
|
||||
if is_builtin_tool_enabled('tasks'):
|
||||
builtin_functions.append(tasks)
|
||||
|
||||
# Automation tools - create and manage scheduled automations from chat
|
||||
if is_builtin_tool_enabled('automations') and 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(
|
||||
func,
|
||||
|
||||
@@ -46,6 +46,10 @@
|
||||
tasks: {
|
||||
label: $i18n.t('Task Management'),
|
||||
description: $i18n.t('Break down complex requests into trackable steps')
|
||||
},
|
||||
automations: {
|
||||
label: $i18n.t('Automations'),
|
||||
description: $i18n.t('Create and manage scheduled automations')
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user