Skip to content

Commit 406251c

Browse files
committed
enh: automation
1 parent ee9db91 commit 406251c

6 files changed

Lines changed: 85 additions & 0 deletions

File tree

backend/open_webui/config.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1541,6 +1541,18 @@ def reachable(host: str, port: int) -> bool:
15411541
os.environ.get('ENABLE_CHANNELS', 'False').lower() == 'true',
15421542
)
15431543

1544+
AUTOMATION_MAX_COUNT = PersistentConfig(
1545+
'AUTOMATION_MAX_COUNT',
1546+
'automations.max_count',
1547+
os.environ.get('AUTOMATION_MAX_COUNT', ''),
1548+
)
1549+
1550+
AUTOMATION_MIN_INTERVAL = PersistentConfig(
1551+
'AUTOMATION_MIN_INTERVAL',
1552+
'automations.min_interval',
1553+
os.environ.get('AUTOMATION_MIN_INTERVAL', ''),
1554+
)
1555+
15441556
ENABLE_NOTES = PersistentConfig(
15451557
'ENABLE_NOTES',
15461558
'notes.enable',

backend/open_webui/main.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,8 @@
383383
API_KEYS_ALLOWED_ENDPOINTS,
384384
ENABLE_FOLDERS,
385385
FOLDER_MAX_FILE_COUNT,
386+
AUTOMATION_MAX_COUNT,
387+
AUTOMATION_MIN_INTERVAL,
386388
ENABLE_CHANNELS,
387389
ENABLE_NOTES,
388390
ENABLE_USER_STATUS,
@@ -874,6 +876,8 @@ async def lifespan(app: FastAPI):
874876

875877
app.state.config.ENABLE_FOLDERS = ENABLE_FOLDERS
876878
app.state.config.FOLDER_MAX_FILE_COUNT = FOLDER_MAX_FILE_COUNT
879+
app.state.config.AUTOMATION_MAX_COUNT = AUTOMATION_MAX_COUNT
880+
app.state.config.AUTOMATION_MIN_INTERVAL = AUTOMATION_MIN_INTERVAL
877881
app.state.config.ENABLE_CHANNELS = ENABLE_CHANNELS
878882
app.state.config.ENABLE_NOTES = ENABLE_NOTES
879883
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING

backend/open_webui/models/automations.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ def insert(
143143
db.refresh(row)
144144
return AutomationModel.model_validate(row)
145145

146+
def count_by_user(self, user_id: str, db: Optional[Session] = None) -> int:
147+
with get_db_context(db) as db:
148+
return db.query(Automation).filter_by(user_id=user_id).count()
149+
146150
def get_by_id(self, id: str, db: Optional[Session] = None) -> Optional[AutomationModel]:
147151
with get_db_context(db) as db:
148152
row = db.get(Automation, id)

backend/open_webui/routers/auths.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,8 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)):
950950
'ENABLE_MESSAGE_RATING': request.app.state.config.ENABLE_MESSAGE_RATING,
951951
'ENABLE_FOLDERS': request.app.state.config.ENABLE_FOLDERS,
952952
'FOLDER_MAX_FILE_COUNT': request.app.state.config.FOLDER_MAX_FILE_COUNT,
953+
'AUTOMATION_MAX_COUNT': request.app.state.config.AUTOMATION_MAX_COUNT,
954+
'AUTOMATION_MIN_INTERVAL': request.app.state.config.AUTOMATION_MIN_INTERVAL,
953955
'ENABLE_CHANNELS': request.app.state.config.ENABLE_CHANNELS,
954956
'ENABLE_MEMORIES': request.app.state.config.ENABLE_MEMORIES,
955957
'ENABLE_NOTES': request.app.state.config.ENABLE_NOTES,
@@ -976,6 +978,8 @@ class AdminConfig(BaseModel):
976978
ENABLE_MESSAGE_RATING: bool
977979
ENABLE_FOLDERS: bool
978980
FOLDER_MAX_FILE_COUNT: Optional[int | str] = None
981+
AUTOMATION_MAX_COUNT: Optional[int | str] = None
982+
AUTOMATION_MIN_INTERVAL: Optional[int | str] = None
979983
ENABLE_CHANNELS: bool
980984
ENABLE_MEMORIES: bool
981985
ENABLE_NOTES: bool
@@ -1001,6 +1005,12 @@ async def update_admin_config(request: Request, form_data: AdminConfig, user=Dep
10011005
request.app.state.config.FOLDER_MAX_FILE_COUNT = (
10021006
int(form_data.FOLDER_MAX_FILE_COUNT) if form_data.FOLDER_MAX_FILE_COUNT else ''
10031007
)
1008+
request.app.state.config.AUTOMATION_MAX_COUNT = (
1009+
int(form_data.AUTOMATION_MAX_COUNT) if form_data.AUTOMATION_MAX_COUNT else ''
1010+
)
1011+
request.app.state.config.AUTOMATION_MIN_INTERVAL = (
1012+
int(form_data.AUTOMATION_MIN_INTERVAL) if form_data.AUTOMATION_MIN_INTERVAL else ''
1013+
)
10041014
request.app.state.config.ENABLE_CHANNELS = form_data.ENABLE_CHANNELS
10051015
request.app.state.config.ENABLE_MEMORIES = form_data.ENABLE_MEMORIES
10061016
request.app.state.config.ENABLE_NOTES = form_data.ENABLE_NOTES
@@ -1042,6 +1052,8 @@ async def update_admin_config(request: Request, form_data: AdminConfig, user=Dep
10421052
'ENABLE_MESSAGE_RATING': request.app.state.config.ENABLE_MESSAGE_RATING,
10431053
'ENABLE_FOLDERS': request.app.state.config.ENABLE_FOLDERS,
10441054
'FOLDER_MAX_FILE_COUNT': request.app.state.config.FOLDER_MAX_FILE_COUNT,
1055+
'AUTOMATION_MAX_COUNT': request.app.state.config.AUTOMATION_MAX_COUNT,
1056+
'AUTOMATION_MIN_INTERVAL': request.app.state.config.AUTOMATION_MIN_INTERVAL,
10451057
'ENABLE_CHANNELS': request.app.state.config.ENABLE_CHANNELS,
10461058
'ENABLE_MEMORIES': request.app.state.config.ENABLE_MEMORIES,
10471059
'ENABLE_NOTES': request.app.state.config.ENABLE_NOTES,

backend/open_webui/routers/automations.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
next_run_ns,
2020
next_n_runs_ns,
2121
execute_automation,
22+
rrule_interval_seconds,
2223
)
2324
from open_webui.utils.auth import get_verified_user, get_admin_user
2425
from open_webui.utils.access_control import has_permission
@@ -60,6 +61,35 @@ def check_automation_access(automation, user):
6061
)
6162

6263

64+
def check_automation_limits(request, user, rrule_str: str, db, is_create: bool = False):
65+
"""Enforce global automation limits. Admins bypass all checks."""
66+
if user.role == 'admin':
67+
return
68+
69+
# Max count (create only)
70+
if is_create:
71+
max_count = request.app.state.config.AUTOMATION_MAX_COUNT
72+
if max_count:
73+
max_count = int(max_count)
74+
if max_count > 0 and Automations.count_by_user(user.id, db=db) >= max_count:
75+
raise HTTPException(
76+
status_code=status.HTTP_403_FORBIDDEN,
77+
detail=f'Automation limit reached ({max_count})',
78+
)
79+
80+
# Min interval (create + update)
81+
min_interval = request.app.state.config.AUTOMATION_MIN_INTERVAL
82+
if min_interval:
83+
min_interval = int(min_interval)
84+
if min_interval > 0:
85+
interval = rrule_interval_seconds(rrule_str)
86+
if interval is not None and interval < min_interval:
87+
raise HTTPException(
88+
status_code=status.HTTP_400_BAD_REQUEST,
89+
detail=f'Schedule too frequent. Minimum interval is {min_interval} seconds.',
90+
)
91+
92+
6393
def enrich_automation(automation: AutomationModel, db: Session, tz: str = None) -> AutomationResponse:
6494
"""Full enrichment for single-item views (includes next_runs computation)."""
6595
last_run = AutomationRuns.get_latest(automation.id, db=db)
@@ -135,6 +165,8 @@ async def create_new_automation(
135165
detail=str(e),
136166
)
137167

168+
check_automation_limits(request, user, form_data.data.rrule, db, is_create=True)
169+
138170
# Validate terminal server exists if linked
139171
if form_data.data.terminal and form_data.data.terminal.server_id:
140172
connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
@@ -192,6 +224,8 @@ async def update_automation_by_id(
192224
detail=str(e),
193225
)
194226

227+
check_automation_limits(request, user, form_data.data.rrule, db, is_create=False)
228+
195229
# Validate terminal server exists if linked
196230
if form_data.data.terminal and form_data.data.terminal.server_id:
197231
connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []

backend/open_webui/utils/automations.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,25 @@ def next_n_runs_ns(s: str, n: int = 5, tz: str = None) -> list[int]:
9292
return result
9393

9494

95+
def rrule_interval_seconds(s: str) -> Optional[int]:
96+
"""Approximate interval between recurrences in seconds.
97+
98+
Returns None for one-shot (COUNT=1) schedules or rules
99+
with fewer than two future occurrences.
100+
"""
101+
if 'COUNT=1' in s:
102+
return None
103+
rule = _parse_rule(s)
104+
now = datetime.now()
105+
first = rule.after(now)
106+
if first is None:
107+
return None
108+
second = rule.after(first)
109+
if second is None:
110+
return None
111+
return int((second - first).total_seconds())
112+
113+
95114
############################
96115
# Worker Loop
97116
############################

0 commit comments

Comments
 (0)