diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 3958c9b148..1b1ae84dfa 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -2081,10 +2081,15 @@ ENABLE_AUTOMATIONS = os.getenv('ENABLE_AUTOMATIONS', 'True').lower() == 'true' ENABLE_SUBAGENTS = os.getenv('ENABLE_SUBAGENTS', 'False').lower() == 'true' SUBAGENTS_BACKGROUND_ENABLED = os.getenv('SUBAGENTS_BACKGROUND_ENABLED', 'False').lower() == 'true' -SUBAGENTS_MAX_CONCURRENT = int(os.getenv('SUBAGENTS_MAX_CONCURRENT', '20')) -SUBAGENTS_MAX_ASYNC = int(os.getenv('SUBAGENTS_MAX_ASYNC', '20')) -SUBAGENTS_MAX_ITERATIONS = int(os.getenv('SUBAGENTS_MAX_ITERATIONS', '30')) -SUBAGENTS_MAX_OUTPUT = int(os.getenv('SUBAGENTS_MAX_OUTPUT', '30000')) +# Leave empty for unlimited. +SUBAGENTS_MAX_CONCURRENT = os.getenv('SUBAGENTS_MAX_CONCURRENT', '') +SUBAGENTS_MAX_CONCURRENT = int(SUBAGENTS_MAX_CONCURRENT) if SUBAGENTS_MAX_CONCURRENT else None +SUBAGENTS_MAX_ASYNC = os.getenv('SUBAGENTS_MAX_ASYNC', '') +SUBAGENTS_MAX_ASYNC = int(SUBAGENTS_MAX_ASYNC) if SUBAGENTS_MAX_ASYNC else None +SUBAGENTS_MAX_ITERATIONS = os.getenv('SUBAGENTS_MAX_ITERATIONS', '') +SUBAGENTS_MAX_ITERATIONS = int(SUBAGENTS_MAX_ITERATIONS) if SUBAGENTS_MAX_ITERATIONS else None +SUBAGENTS_MAX_OUTPUT = os.getenv('SUBAGENTS_MAX_OUTPUT', '') +SUBAGENTS_MAX_OUTPUT = int(SUBAGENTS_MAX_OUTPUT) if SUBAGENTS_MAX_OUTPUT else None SUBAGENTS_SYSTEM_PROMPT = os.getenv('SUBAGENTS_SYSTEM_PROMPT', '') AUTOMATION_MAX_COUNT = os.getenv('AUTOMATION_MAX_COUNT', '') diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index eb42f230be..1f57f3dd6c 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -31,7 +31,7 @@ from open_webui.utils.tools import ( set_terminal_servers, set_tool_servers, ) -from pydantic import BaseModel, ConfigDict, StrictBool +from pydantic import BaseModel, ConfigDict, Field, StrictBool, field_validator router = APIRouter() @@ -799,12 +799,28 @@ async def set_models_config(request: Request, form_data: ModelsConfigForm, user= class SubagentsConfigForm(BaseModel): ENABLE_SUBAGENTS: bool SUBAGENTS_BACKGROUND_ENABLED: bool - SUBAGENTS_MAX_CONCURRENT: int - SUBAGENTS_MAX_ASYNC: int - SUBAGENTS_MAX_ITERATIONS: int - SUBAGENTS_MAX_OUTPUT: int + SUBAGENTS_MAX_CONCURRENT: int | None = Field(default=None, gt=0) + SUBAGENTS_MAX_ASYNC: int | None = Field(default=None, gt=0) + SUBAGENTS_MAX_ITERATIONS: int | None = Field(default=None, gt=0) + SUBAGENTS_MAX_OUTPUT: int | None = Field(default=None, gt=0) SUBAGENTS_SYSTEM_PROMPT: str + @field_validator( + 'SUBAGENTS_MAX_CONCURRENT', + 'SUBAGENTS_MAX_ASYNC', + 'SUBAGENTS_MAX_ITERATIONS', + 'SUBAGENTS_MAX_OUTPUT', + mode='before', + ) + @classmethod + def normalize_limit(cls, value): + return None if value == '' else value + + @field_validator('SUBAGENTS_MAX_CONCURRENT', 'SUBAGENTS_MAX_ASYNC', mode='before') + @classmethod + def normalize_legacy_unlimited(cls, value): + return None if value == -1 else value + @router.get('/subagents', response_model=SubagentsConfigForm) async def get_subagents_config(user=Depends(get_admin_user)): diff --git a/backend/open_webui/utils/subagents.py b/backend/open_webui/utils/subagents.py index 530ac3864a..9d237d2e0f 100644 --- a/backend/open_webui/utils/subagents.py +++ b/backend/open_webui/utils/subagents.py @@ -322,13 +322,21 @@ async def delegate( 'subagents.max_output', 'subagents.system_prompt', ) - max_concurrent = int(config.get('subagents.max_concurrent') or 20) - max_async = int(config.get('subagents.max_async') or 20) - max_iterations = int(config.get('subagents.max_iterations') or 30) - max_output = int(config.get('subagents.max_output') or 30_000) - if max_concurrent != -1: + max_concurrent = config.get('subagents.max_concurrent') + max_concurrent = int(max_concurrent) if max_concurrent else None + max_async = config.get('subagents.max_async') + max_async = int(max_async) if max_async else None + max_iterations = config.get('subagents.max_iterations') + max_iterations = int(max_iterations) if max_iterations else None + max_output = config.get('subagents.max_output') + max_output = int(max_output) if max_output else None + if max_concurrent == -1: + max_concurrent = None + elif max_concurrent is not None: max_concurrent = max(1, max_concurrent) - if max_async != -1: + if max_async == -1: + max_async = None + elif max_async is not None: max_async = max(1, max_async) if background and not config.get('subagents.background_enabled'): @@ -406,13 +414,13 @@ async def delegate( foreground_semaphore = None if background: async with _background_lock: - if max_async != -1 and len(_background_active) >= max_async: + if max_async is not None and len(_background_active) >= max_async: return ( f'Error: Async subagent capacity reached ({max_async} running). ' 'Wait for one to finish or increase subagents.max_async.' ) _background_active.add(delegation_id) - elif max_concurrent != -1: + elif max_concurrent is not None: if _foreground_semaphore is None: _foreground_semaphore = asyncio.Semaphore(max_concurrent) foreground_semaphore = _foreground_semaphore @@ -551,7 +559,7 @@ async def delegate( for part in item.get('content') or [] if part.get('type') == 'output_text' ) - if len(summary) > max_output: + if max_output is not None and len(summary) > max_output: summary = f'{summary[:max_output]}\n\n[output truncated]' error = message.get('error') if isinstance(error, dict): diff --git a/src/lib/components/admin/Settings/Subagents.svelte b/src/lib/components/admin/Settings/Subagents.svelte index a1a490c4f2..574d82351b 100644 --- a/src/lib/components/admin/Settings/Subagents.svelte +++ b/src/lib/components/admin/Settings/Subagents.svelte @@ -15,10 +15,10 @@ let saving = false; let enabled = false; let backgroundEnabled = false; - let maxConcurrent = 20; - let maxAsync = 20; - let maxIterations = 30; - let maxOutput = 30000; + let maxConcurrent: number | null = null; + let maxAsync: number | null = null; + let maxIterations: number | null = null; + let maxOutput: number | null = null; let systemPrompt = ''; onMount(async () => { @@ -26,10 +26,10 @@ const config = await getSubagentsConfig(localStorage.token); enabled = config?.ENABLE_SUBAGENTS ?? false; backgroundEnabled = config?.SUBAGENTS_BACKGROUND_ENABLED ?? false; - maxConcurrent = Number(config?.SUBAGENTS_MAX_CONCURRENT) || 20; - maxAsync = Number(config?.SUBAGENTS_MAX_ASYNC) || 20; - maxIterations = Number(config?.SUBAGENTS_MAX_ITERATIONS) || 30; - maxOutput = Number(config?.SUBAGENTS_MAX_OUTPUT) || 30000; + maxConcurrent = config?.SUBAGENTS_MAX_CONCURRENT ?? null; + maxAsync = config?.SUBAGENTS_MAX_ASYNC ?? null; + maxIterations = config?.SUBAGENTS_MAX_ITERATIONS ?? null; + maxOutput = config?.SUBAGENTS_MAX_OUTPUT ?? null; systemPrompt = config?.SUBAGENTS_SYSTEM_PROMPT ?? ''; } catch (error) { toast.error(`${error}`); @@ -44,10 +44,10 @@ await setSubagentsConfig(localStorage.token, { ENABLE_SUBAGENTS: enabled, SUBAGENTS_BACKGROUND_ENABLED: backgroundEnabled, - SUBAGENTS_MAX_CONCURRENT: maxConcurrent, - SUBAGENTS_MAX_ASYNC: maxAsync, - SUBAGENTS_MAX_ITERATIONS: maxIterations, - SUBAGENTS_MAX_OUTPUT: maxOutput, + SUBAGENTS_MAX_CONCURRENT: maxConcurrent ?? null, + SUBAGENTS_MAX_ASYNC: maxAsync ?? null, + SUBAGENTS_MAX_ITERATIONS: maxIterations ?? null, + SUBAGENTS_MAX_OUTPUT: maxOutput ?? null, SUBAGENTS_SYSTEM_PROMPT: systemPrompt }); await appConfig.set(await getBackendConfig()); @@ -92,13 +92,21 @@ id="sa-concurrent" type="number" bind:value={maxConcurrent} - min="-1" + min="1" + step="1" + aria-describedby="sa-concurrent-help" class="h-7 w-16 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500" /> {$i18n.t('simultaneous sub-agents')} +
+ {$i18n.t('Leave empty for unlimited')} +
+ {$i18n.t('Leave empty for unlimited')} +
{/if} @@ -143,13 +156,20 @@ type="number" bind:value={maxIterations} min="1" - max="100" + step="1" + aria-describedby="sa-iterations-help" class="h-7 w-16 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500" /> {$i18n.t('tool loops per sub-agent')} ++ {$i18n.t('Leave empty for unlimited')} +
+ {$i18n.t('Leave empty for unlimited')} +