enh: dynamic select options valve
This commit is contained in:
@@ -19,6 +19,7 @@ from open_webui.utils.plugin import (
|
||||
load_function_module_by_id,
|
||||
replace_imports,
|
||||
get_function_module_from_cache,
|
||||
resolve_valves_schema_options,
|
||||
)
|
||||
from open_webui.config import CACHE_DIR
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
@@ -446,7 +447,10 @@ async def get_function_valves_spec_by_id(
|
||||
|
||||
if hasattr(function_module, "Valves"):
|
||||
Valves = function_module.Valves
|
||||
return Valves.schema()
|
||||
schema = Valves.schema()
|
||||
# Resolve dynamic options for select dropdowns
|
||||
schema = resolve_valves_schema_options(Valves, schema, user)
|
||||
return schema
|
||||
return None
|
||||
else:
|
||||
raise HTTPException(
|
||||
@@ -546,7 +550,10 @@ async def get_function_user_valves_spec_by_id(
|
||||
|
||||
if hasattr(function_module, "UserValves"):
|
||||
UserValves = function_module.UserValves
|
||||
return UserValves.schema()
|
||||
schema = UserValves.schema()
|
||||
# Resolve dynamic options for select dropdowns
|
||||
schema = resolve_valves_schema_options(UserValves, schema, user)
|
||||
return schema
|
||||
return None
|
||||
else:
|
||||
raise HTTPException(
|
||||
|
||||
@@ -25,6 +25,7 @@ from open_webui.utils.plugin import (
|
||||
load_tool_module_by_id,
|
||||
replace_imports,
|
||||
get_tool_module_from_cache,
|
||||
resolve_valves_schema_options,
|
||||
)
|
||||
from open_webui.utils.tools import get_tool_specs
|
||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||
@@ -553,7 +554,10 @@ async def get_tools_valves_spec_by_id(
|
||||
|
||||
if hasattr(tools_module, "Valves"):
|
||||
Valves = tools_module.Valves
|
||||
return Valves.schema()
|
||||
schema = Valves.schema()
|
||||
# Resolve dynamic options for select dropdowns
|
||||
schema = resolve_valves_schema_options(Valves, schema, user)
|
||||
return schema
|
||||
return None
|
||||
else:
|
||||
raise HTTPException(
|
||||
@@ -662,7 +666,10 @@ async def get_tools_user_valves_spec_by_id(
|
||||
|
||||
if hasattr(tools_module, "UserValves"):
|
||||
UserValves = tools_module.UserValves
|
||||
return UserValves.schema()
|
||||
schema = UserValves.schema()
|
||||
# Resolve dynamic options for select dropdowns
|
||||
schema = resolve_valves_schema_options(UserValves, schema, user)
|
||||
return schema
|
||||
return None
|
||||
else:
|
||||
raise HTTPException(
|
||||
|
||||
@@ -6,6 +6,7 @@ from importlib import util
|
||||
import types
|
||||
import tempfile
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from open_webui.env import PIP_OPTIONS, PIP_PACKAGE_INDEX_OPTIONS, OFFLINE_MODE
|
||||
from open_webui.models.functions import Functions
|
||||
@@ -14,6 +15,143 @@ from open_webui.models.tools import Tools
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def resolve_valves_schema_options(
|
||||
valves_class: type, schema: dict, user: Any = None
|
||||
) -> dict:
|
||||
"""
|
||||
Resolve dynamic options in a Valves schema.
|
||||
|
||||
For properties with `input.options`, this function handles two cases:
|
||||
- List: Used directly as dropdown options
|
||||
- String: Treated as method name, called to get options dynamically
|
||||
|
||||
Usage in Valves:
|
||||
class UserValves(BaseModel):
|
||||
# Static options
|
||||
priority: str = Field(
|
||||
default="medium",
|
||||
json_schema_extra={
|
||||
"input": {
|
||||
"type": "select",
|
||||
"options": ["low", "medium", "high"]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# Dynamic options (method name)
|
||||
model: str = Field(
|
||||
default="",
|
||||
json_schema_extra={
|
||||
"input": {
|
||||
"type": "select",
|
||||
"options": "get_model_options"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_model_options(cls, __user__=None) -> list[dict]:
|
||||
return [{"value": "gpt-4", "label": "GPT-4"}]
|
||||
|
||||
Args:
|
||||
valves_class: The Valves or UserValves Pydantic model class
|
||||
schema: The JSON schema dict from valves_class.schema()
|
||||
user: Optional user object passed to methods that accept __user__
|
||||
|
||||
Returns:
|
||||
Modified schema dict with resolved options
|
||||
"""
|
||||
if not schema or "properties" not in schema:
|
||||
return schema
|
||||
|
||||
# Make a copy to avoid mutating the original
|
||||
schema = dict(schema)
|
||||
schema["properties"] = dict(schema.get("properties", {}))
|
||||
|
||||
for prop_name, prop_schema in list(schema["properties"].items()):
|
||||
# Get the original field info from the Pydantic model
|
||||
if not hasattr(valves_class, "model_fields"):
|
||||
continue
|
||||
|
||||
field_info = valves_class.model_fields.get(prop_name)
|
||||
if not field_info:
|
||||
continue
|
||||
|
||||
# Check json_schema_extra for options
|
||||
json_schema_extra = field_info.json_schema_extra
|
||||
if not json_schema_extra or not isinstance(json_schema_extra, dict):
|
||||
continue
|
||||
|
||||
input_config = json_schema_extra.get("input")
|
||||
if not input_config or not isinstance(input_config, dict):
|
||||
continue
|
||||
|
||||
options = input_config.get("options")
|
||||
if options is None:
|
||||
continue
|
||||
|
||||
resolved_options = None
|
||||
|
||||
# Case 1: options is already a list - use directly
|
||||
if isinstance(options, list):
|
||||
resolved_options = options
|
||||
|
||||
# Case 2: options is a string - treat as method name
|
||||
elif isinstance(options, str) and options:
|
||||
method = getattr(valves_class, options, None)
|
||||
if method is None or not callable(method):
|
||||
log.warning(
|
||||
f"options '{options}' not found or not callable on {valves_class.__name__}"
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
import inspect
|
||||
|
||||
sig = inspect.signature(method)
|
||||
params = sig.parameters
|
||||
|
||||
# Prepare kwargs based on what the method accepts
|
||||
kwargs = {}
|
||||
if "__user__" in params and user is not None:
|
||||
kwargs["__user__"] = (
|
||||
user.model_dump() if hasattr(user, "model_dump") else user
|
||||
)
|
||||
if "user" in params and user is not None:
|
||||
kwargs["user"] = (
|
||||
user.model_dump() if hasattr(user, "model_dump") else user
|
||||
)
|
||||
|
||||
resolved_options = method(**kwargs) if kwargs else method()
|
||||
|
||||
# Validate return type
|
||||
if not isinstance(resolved_options, list):
|
||||
log.warning(
|
||||
f"Method '{options}' did not return a list for {prop_name}"
|
||||
)
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
log.warning(f"Failed to resolve options for {prop_name}: {e}")
|
||||
continue
|
||||
else:
|
||||
# Invalid options type - skip
|
||||
continue
|
||||
|
||||
# Update the schema with resolved options
|
||||
schema["properties"][prop_name] = dict(prop_schema)
|
||||
if "input" not in schema["properties"][prop_name]:
|
||||
schema["properties"][prop_name]["input"] = {"type": "select"}
|
||||
else:
|
||||
schema["properties"][prop_name]["input"] = dict(
|
||||
schema["properties"][prop_name].get("input", {})
|
||||
)
|
||||
schema["properties"][prop_name]["input"]["options"] = resolved_options
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
|
||||
def extract_frontmatter(content):
|
||||
"""
|
||||
Extract frontmatter as a dictionary from the provided content string.
|
||||
|
||||
@@ -116,6 +116,27 @@
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else if valvesSpec.properties[property]?.input?.type === 'select' && valvesSpec.properties[property]?.input?.options}
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||
bind:value={valves[property]}
|
||||
on:change={() => {
|
||||
dispatch('change');
|
||||
}}
|
||||
>
|
||||
<option value="" disabled>{valvesSpec.properties[property]?.description ?? $i18n.t('Select an option')}</option>
|
||||
{#each valvesSpec.properties[property].input.options as option}
|
||||
{#if typeof option === 'object' && option !== null}
|
||||
<option value={option.value} selected={option.value === valves[property]}>
|
||||
{option.label ?? option.value}
|
||||
</option>
|
||||
{:else}
|
||||
<option value={option} selected={option === valves[property]}>
|
||||
{option}
|
||||
</option>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
{:else if valvesSpec.properties[property]?.input?.type === 'color'}
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="relative size-6">
|
||||
|
||||
Reference in New Issue
Block a user