refac
This commit is contained in:
@@ -2494,3 +2494,332 @@ async def tasks(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(f'tasks error: {e}')
|
log.exception(f'tasks error: {e}')
|
||||||
return json.dumps({'error': str(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_knowledge_file,
|
||||||
view_skill,
|
view_skill,
|
||||||
tasks,
|
tasks,
|
||||||
|
create_automation,
|
||||||
|
update_automation,
|
||||||
|
list_automations,
|
||||||
|
toggle_automation,
|
||||||
|
delete_automation,
|
||||||
)
|
)
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
from open_webui.utils.access_control import has_permission
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -397,6 +403,18 @@ def get_builtin_tools(
|
|||||||
builtin_tools = model.get('info', {}).get('meta', {}).get('builtinTools', {})
|
builtin_tools = model.get('info', {}).get('meta', {}).get('builtinTools', {})
|
||||||
return builtin_tools.get(category, True)
|
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
|
# Time utilities - available for date calculations
|
||||||
if is_builtin_tool_enabled('time'):
|
if is_builtin_tool_enabled('time'):
|
||||||
builtin_functions.extend([get_current_timestamp, calculate_timestamp])
|
builtin_functions.extend([get_current_timestamp, calculate_timestamp])
|
||||||
@@ -440,7 +458,11 @@ def get_builtin_tools(
|
|||||||
builtin_functions.extend([search_chats, view_chat])
|
builtin_functions.extend([search_chats, view_chat])
|
||||||
|
|
||||||
# Add memory tools if builtin category enabled AND enabled for this 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(
|
builtin_functions.extend(
|
||||||
[
|
[
|
||||||
search_memories,
|
search_memories,
|
||||||
@@ -457,6 +479,7 @@ def get_builtin_tools(
|
|||||||
and getattr(request.app.state.config, 'ENABLE_WEB_SEARCH', False)
|
and getattr(request.app.state.config, 'ENABLE_WEB_SEARCH', False)
|
||||||
and get_model_capability('web_search')
|
and get_model_capability('web_search')
|
||||||
and features.get('web_search')
|
and features.get('web_search')
|
||||||
|
and has_user_permission('web_search')
|
||||||
):
|
):
|
||||||
builtin_functions.extend([search_web, fetch_url])
|
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 getattr(request.app.state.config, 'ENABLE_IMAGE_GENERATION', False)
|
||||||
and get_model_capability('image_generation')
|
and get_model_capability('image_generation')
|
||||||
and features.get('image_generation')
|
and features.get('image_generation')
|
||||||
|
and has_user_permission('image_generation')
|
||||||
):
|
):
|
||||||
builtin_functions.append(generate_image)
|
builtin_functions.append(generate_image)
|
||||||
if (
|
if (
|
||||||
@@ -473,6 +497,7 @@ def get_builtin_tools(
|
|||||||
and getattr(request.app.state.config, 'ENABLE_IMAGE_EDIT', False)
|
and getattr(request.app.state.config, 'ENABLE_IMAGE_EDIT', False)
|
||||||
and get_model_capability('image_generation')
|
and get_model_capability('image_generation')
|
||||||
and features.get('image_generation')
|
and features.get('image_generation')
|
||||||
|
and has_user_permission('image_generation')
|
||||||
):
|
):
|
||||||
builtin_functions.append(edit_image)
|
builtin_functions.append(edit_image)
|
||||||
|
|
||||||
@@ -482,15 +507,24 @@ def get_builtin_tools(
|
|||||||
and getattr(request.app.state.config, 'ENABLE_CODE_INTERPRETER', True)
|
and getattr(request.app.state.config, 'ENABLE_CODE_INTERPRETER', True)
|
||||||
and get_model_capability('code_interpreter')
|
and get_model_capability('code_interpreter')
|
||||||
and features.get('code_interpreter')
|
and features.get('code_interpreter')
|
||||||
|
and has_user_permission('code_interpreter')
|
||||||
):
|
):
|
||||||
builtin_functions.append(execute_code)
|
builtin_functions.append(execute_code)
|
||||||
|
|
||||||
# Notes tools - search, view, create, and update user's notes (if builtin category enabled AND notes enabled globally)
|
# 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):
|
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])
|
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)
|
# Channels tools - search channels and messages
|
||||||
if is_builtin_tool_enabled('channels') and getattr(request.app.state.config, 'ENABLE_CHANNELS', False):
|
if (
|
||||||
|
is_builtin_tool_enabled('channels')
|
||||||
|
and getattr(request.app.state.config, 'ENABLE_CHANNELS', False)
|
||||||
|
and has_user_permission('channels')
|
||||||
|
):
|
||||||
builtin_functions.extend(
|
builtin_functions.extend(
|
||||||
[
|
[
|
||||||
search_channels,
|
search_channels,
|
||||||
@@ -508,6 +542,10 @@ def get_builtin_tools(
|
|||||||
if is_builtin_tool_enabled('tasks'):
|
if is_builtin_tool_enabled('tasks'):
|
||||||
builtin_functions.append(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:
|
for func in builtin_functions:
|
||||||
callable = get_async_tool_function_and_apply_extra_params(
|
callable = get_async_tool_function_and_apply_extra_params(
|
||||||
func,
|
func,
|
||||||
|
|||||||
@@ -46,6 +46,10 @@
|
|||||||
tasks: {
|
tasks: {
|
||||||
label: $i18n.t('Task Management'),
|
label: $i18n.t('Task Management'),
|
||||||
description: $i18n.t('Break down complex requests into trackable steps')
|
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