@@ -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