Skip to content

Commit 6746959

Browse files
committed
refac
1 parent 588b81e commit 6746959

3 files changed

Lines changed: 376 additions & 5 deletions

File tree

backend/open_webui/tools/builtin.py

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2494,3 +2494,332 @@ def _resolve_id(d: dict, idx: int) -> str:
24942494
except Exception as e:
24952495
log.exception(f'tasks error: {e}')
24962496
return json.dumps({'error': str(e)})
2497+
2498+
2499+
# =============================================================================
2500+
# AUTOMATION TOOLS
2501+
# =============================================================================
2502+
2503+
2504+
async def create_automation(
2505+
name: str,
2506+
prompt: str,
2507+
rrule: str,
2508+
model_id: Optional[str] = None,
2509+
__request__: Request = None,
2510+
__user__: dict = None,
2511+
__metadata__: dict = None,
2512+
) -> str:
2513+
"""
2514+
Create a scheduled automation that runs a prompt on a recurring or one-time schedule.
2515+
Use this when the user wants to schedule a task to run automatically.
2516+
2517+
The rrule parameter must be a valid iCalendar RRULE string. Common examples:
2518+
- Every day at 9am: "DTSTART:20250101T090000\\nRRULE:FREQ=DAILY"
2519+
- Every Monday at 8am: "DTSTART:20250106T080000\\nRRULE:FREQ=WEEKLY;BYDAY=MO"
2520+
- Every hour: "RRULE:FREQ=HOURLY;INTERVAL=1"
2521+
- Every 30 minutes: "RRULE:FREQ=MINUTELY;INTERVAL=30"
2522+
- Once at a specific time: "DTSTART:20250415T140000\\nRRULE:FREQ=DAILY;COUNT=1"
2523+
- First day of every month: "DTSTART:20250101T090000\\nRRULE:FREQ=MONTHLY;BYMONTHDAY=1"
2524+
2525+
The DTSTART time should reflect the desired execution time. Use COUNT=1 for one-time automations.
2526+
2527+
:param name: A short descriptive name for the automation
2528+
:param prompt: The prompt/instructions to execute on each run
2529+
:param rrule: An iCalendar RRULE string defining the schedule
2530+
:param model_id: Optional model ID to use. Defaults to the current chat model if omitted.
2531+
:return: JSON with the created automation details including id, next scheduled runs
2532+
"""
2533+
if __request__ is None:
2534+
return json.dumps({'error': 'Request context not available'})
2535+
2536+
if not __user__:
2537+
return json.dumps({'error': 'User context not available'})
2538+
2539+
try:
2540+
from open_webui.models.automations import Automations, AutomationForm, AutomationData
2541+
from open_webui.models.users import Users
2542+
from open_webui.utils.automations import validate_rrule, next_run_ns, next_n_runs_ns
2543+
2544+
user_id = __user__.get('id')
2545+
user = Users.get_user_by_id(user_id)
2546+
if not user:
2547+
return json.dumps({'error': 'User not found'})
2548+
2549+
# Default to current chat's model if not specified
2550+
if not model_id:
2551+
model_id = (__metadata__ or {}).get('model_id') or (__metadata__ or {}).get('model')
2552+
if not model_id:
2553+
return json.dumps({'error': 'model_id is required (could not detect current model)'})
2554+
2555+
# Validate the RRULE
2556+
try:
2557+
validate_rrule(rrule)
2558+
except ValueError as e:
2559+
return json.dumps({'error': f'Invalid schedule: {e}'})
2560+
2561+
tz = user.timezone
2562+
form = AutomationForm(
2563+
name=name,
2564+
data=AutomationData(
2565+
prompt=prompt,
2566+
model_id=model_id,
2567+
rrule=rrule,
2568+
),
2569+
is_active=True,
2570+
)
2571+
2572+
automation = Automations.insert(user_id, form, next_run_ns(rrule, tz=tz))
2573+
2574+
return json.dumps(
2575+
{
2576+
'status': 'success',
2577+
'id': automation.id,
2578+
'name': automation.name,
2579+
'model_id': model_id,
2580+
'is_active': automation.is_active,
2581+
'next_runs': next_n_runs_ns(rrule, tz=tz),
2582+
},
2583+
ensure_ascii=False,
2584+
)
2585+
except Exception as e:
2586+
log.exception(f'create_automation error: {e}')
2587+
return json.dumps({'error': str(e)})
2588+
2589+
2590+
async def update_automation(
2591+
automation_id: str,
2592+
name: Optional[str] = None,
2593+
prompt: Optional[str] = None,
2594+
rrule: Optional[str] = None,
2595+
model_id: Optional[str] = None,
2596+
__request__: Request = None,
2597+
__user__: dict = None,
2598+
) -> str:
2599+
"""
2600+
Update an existing automation. Only the provided fields are changed; omitted fields stay the same.
2601+
2602+
:param automation_id: The ID of the automation to update
2603+
:param name: New name for the automation (optional)
2604+
:param prompt: New prompt/instructions (optional)
2605+
:param rrule: New iCalendar RRULE schedule string (optional). See create_automation for format examples.
2606+
:param model_id: New model ID to use (optional)
2607+
:return: JSON with the updated automation details
2608+
"""
2609+
if __request__ is None:
2610+
return json.dumps({'error': 'Request context not available'})
2611+
2612+
if not __user__:
2613+
return json.dumps({'error': 'User context not available'})
2614+
2615+
try:
2616+
from open_webui.models.automations import Automations, AutomationForm, AutomationData
2617+
from open_webui.models.users import Users
2618+
from open_webui.utils.automations import validate_rrule, next_run_ns, next_n_runs_ns
2619+
2620+
user_id = __user__.get('id')
2621+
user = Users.get_user_by_id(user_id)
2622+
2623+
automation = Automations.get_by_id(automation_id)
2624+
if not automation:
2625+
return json.dumps({'error': 'Automation not found'})
2626+
if automation.user_id != user_id:
2627+
return json.dumps({'error': 'Access denied'})
2628+
2629+
# Merge provided fields with existing values
2630+
new_name = name if name is not None else automation.name
2631+
new_prompt = prompt if prompt is not None else automation.data.get('prompt', '')
2632+
new_model_id = model_id if model_id is not None else automation.data.get('model_id', '')
2633+
new_rrule = rrule if rrule is not None else automation.data.get('rrule', '')
2634+
2635+
# Validate RRULE if changed
2636+
if rrule is not None:
2637+
try:
2638+
validate_rrule(new_rrule)
2639+
except ValueError as e:
2640+
return json.dumps({'error': f'Invalid schedule: {e}'})
2641+
2642+
tz = user.timezone if user else None
2643+
form = AutomationForm(
2644+
name=new_name,
2645+
data=AutomationData(
2646+
prompt=new_prompt,
2647+
model_id=new_model_id,
2648+
rrule=new_rrule,
2649+
),
2650+
is_active=automation.is_active,
2651+
)
2652+
2653+
updated = Automations.update(automation_id, form, next_run_ns(new_rrule, tz=tz))
2654+
2655+
return json.dumps(
2656+
{
2657+
'status': 'success',
2658+
'id': updated.id,
2659+
'name': updated.name,
2660+
'model_id': new_model_id,
2661+
'is_active': updated.is_active,
2662+
'next_runs': next_n_runs_ns(new_rrule, tz=tz),
2663+
},
2664+
ensure_ascii=False,
2665+
)
2666+
except Exception as e:
2667+
log.exception(f'update_automation error: {e}')
2668+
return json.dumps({'error': str(e)})
2669+
2670+
2671+
async def list_automations(
2672+
status: Optional[str] = None,
2673+
count: int = 10,
2674+
__request__: Request = None,
2675+
__user__: dict = None,
2676+
) -> str:
2677+
"""
2678+
List the user's scheduled automations.
2679+
2680+
:param status: Filter by status: "active", "paused", or omit for all
2681+
:param count: Maximum number of automations to return (default: 10)
2682+
:return: JSON list of automations with id, name, prompt snippet, schedule, status, and next runs
2683+
"""
2684+
if __request__ is None:
2685+
return json.dumps({'error': 'Request context not available'})
2686+
2687+
if not __user__:
2688+
return json.dumps({'error': 'User context not available'})
2689+
2690+
try:
2691+
from open_webui.models.automations import Automations
2692+
from open_webui.models.users import Users
2693+
from open_webui.utils.automations import next_n_runs_ns
2694+
2695+
user_id = __user__.get('id')
2696+
user = Users.get_user_by_id(user_id)
2697+
2698+
result = Automations.search_automations(
2699+
user_id=user_id,
2700+
status=status,
2701+
skip=0,
2702+
limit=count,
2703+
)
2704+
2705+
automations = []
2706+
for item in result.items:
2707+
rrule = item.data.get('rrule', '')
2708+
prompt_text = item.data.get('prompt', '')
2709+
snippet = prompt_text[:100] + ('...' if len(prompt_text) > 100 else '')
2710+
2711+
automations.append(
2712+
{
2713+
'id': item.id,
2714+
'name': item.name,
2715+
'prompt_snippet': snippet,
2716+
'model_id': item.data.get('model_id', ''),
2717+
'rrule': rrule,
2718+
'is_active': item.is_active,
2719+
'last_run_at': item.last_run_at,
2720+
'next_runs': next_n_runs_ns(rrule, tz=user.timezone if user else None),
2721+
}
2722+
)
2723+
2724+
return json.dumps(
2725+
{'automations': automations, 'total': result.total},
2726+
ensure_ascii=False,
2727+
)
2728+
except Exception as e:
2729+
log.exception(f'list_automations error: {e}')
2730+
return json.dumps({'error': str(e)})
2731+
2732+
2733+
async def toggle_automation(
2734+
automation_id: str,
2735+
__request__: Request = None,
2736+
__user__: dict = None,
2737+
) -> str:
2738+
"""
2739+
Pause or resume a scheduled automation. If active, it will be paused. If paused, it will be resumed.
2740+
2741+
:param automation_id: The ID of the automation to toggle
2742+
:return: JSON with the updated automation status
2743+
"""
2744+
if __request__ is None:
2745+
return json.dumps({'error': 'Request context not available'})
2746+
2747+
if not __user__:
2748+
return json.dumps({'error': 'User context not available'})
2749+
2750+
try:
2751+
from open_webui.models.automations import Automations
2752+
from open_webui.models.users import Users
2753+
from open_webui.utils.automations import next_run_ns
2754+
2755+
user_id = __user__.get('id')
2756+
user = Users.get_user_by_id(user_id)
2757+
2758+
automation = Automations.get_by_id(automation_id)
2759+
if not automation:
2760+
return json.dumps({'error': 'Automation not found'})
2761+
if automation.user_id != user_id:
2762+
return json.dumps({'error': 'Access denied'})
2763+
2764+
rrule = automation.data.get('rrule', '')
2765+
toggled = Automations.toggle(
2766+
automation_id,
2767+
next_run_ns(rrule, tz=user.timezone if user else None),
2768+
)
2769+
2770+
return json.dumps(
2771+
{
2772+
'status': 'success',
2773+
'id': toggled.id,
2774+
'name': toggled.name,
2775+
'is_active': toggled.is_active,
2776+
},
2777+
ensure_ascii=False,
2778+
)
2779+
except Exception as e:
2780+
log.exception(f'toggle_automation error: {e}')
2781+
return json.dumps({'error': str(e)})
2782+
2783+
2784+
async def delete_automation(
2785+
automation_id: str,
2786+
__request__: Request = None,
2787+
__user__: dict = None,
2788+
) -> str:
2789+
"""
2790+
Delete a scheduled automation and all its run history.
2791+
2792+
:param automation_id: The ID of the automation to delete
2793+
:return: JSON confirming the automation was deleted
2794+
"""
2795+
if __request__ is None:
2796+
return json.dumps({'error': 'Request context not available'})
2797+
2798+
if not __user__:
2799+
return json.dumps({'error': 'User context not available'})
2800+
2801+
try:
2802+
from open_webui.models.automations import Automations, AutomationRuns
2803+
2804+
user_id = __user__.get('id')
2805+
2806+
automation = Automations.get_by_id(automation_id)
2807+
if not automation:
2808+
return json.dumps({'error': 'Automation not found'})
2809+
if automation.user_id != user_id:
2810+
return json.dumps({'error': 'Access denied'})
2811+
2812+
name = automation.name
2813+
AutomationRuns.delete_by_automation(automation_id)
2814+
Automations.delete(automation_id)
2815+
2816+
return json.dumps(
2817+
{
2818+
'status': 'success',
2819+
'message': f'Automation "{name}" deleted',
2820+
},
2821+
ensure_ascii=False,
2822+
)
2823+
except Exception as e:
2824+
log.exception(f'delete_automation error: {e}')
2825+
return json.dumps({'error': str(e)})

0 commit comments

Comments
 (0)