enh: dynamic select options valve

This commit is contained in:
Timothy Jaeryang Baek
2026-01-22 03:55:07 +04:00
parent 00b3583dc2
commit 474427c67e
4 changed files with 177 additions and 4 deletions

View File

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