This commit is contained in:
Timothy Jaeryang Baek
2026-08-13 18:13:43 -06:00
parent 2c01d59335
commit 4465f52a3e
6 changed files with 467 additions and 10 deletions
+108
View File
@@ -498,6 +498,114 @@ async def edit_image(
return JSONCodec.dumps({'error': str(e)})
# =============================================================================
# USER INPUT TOOLS
# =============================================================================
async def ask_user(
questions: list[dict],
allow_other: bool = True,
__event_call__: callable = None,
) -> str:
"""
Ask the user clarifying questions before continuing.
Use this when the next step depends on user intent, preference, or a tradeoff that cannot be inferred safely.
:param questions: 1-3 question objects, each with id, header, question, and 2-3 options. Each option needs label and description.
:param allow_other: Whether users may enter a free-form answer instead of choosing one of the options
:return: JSON with status and answers keyed by question id
"""
try:
if not isinstance(questions, list) or not 1 <= len(questions) <= 3:
raise ValueError('ask_user requires 1-3 questions.')
normalized_questions = []
seen_ids = set()
for index, question in enumerate(questions):
if not isinstance(question, dict):
raise ValueError('Each question must be an object.')
question_id = str(question.get('id') or '').strip()[:64]
if not question_id:
raise ValueError('Each question requires a non-empty id.')
if question_id in seen_ids:
raise ValueError(f'Duplicate question id: {question_id}')
seen_ids.add(question_id)
options = question.get('options')
if not isinstance(options, list) or not 2 <= len(options) <= 3:
raise ValueError('Each question requires 2-3 options.')
normalized_options = []
for option in options:
if not isinstance(option, dict):
raise ValueError('Each option must be an object.')
label = str(option.get('label') or '').strip()[:80]
description = str(option.get('description') or '').strip()[:240]
if not label or not description:
raise ValueError('Each option requires a label and description.')
normalized_options.append(
{
'label': label,
'description': description,
}
)
question_text = str(question.get('question') or '').strip()[:500]
if not question_text:
raise ValueError('Each question requires question text.')
normalized_questions.append(
{
'id': question_id,
'header': str(question.get('header') or '').strip()[:48] or f'Question {index + 1}',
'question': question_text,
'options': normalized_options,
'allow_other': bool(question.get('allow_other', allow_other)),
}
)
if __event_call__ is None:
return JSONCodec.dumps(
{
'status': 'error',
'error': 'User input requires an active browser session with WebSocket connection.',
},
ensure_ascii=False,
)
output = await __event_call__(
{
'type': 'request:user_input',
'data': {
'questions': normalized_questions,
'allow_other': allow_other,
},
}
)
if not isinstance(output, dict):
return JSONCodec.dumps({'status': 'error', 'error': 'Invalid user input response.'}, ensure_ascii=False)
if output.get('error'):
return JSONCodec.dumps({'status': 'error', 'error': output.get('error')}, ensure_ascii=False)
if output.get('status') == 'cancelled':
return JSONCodec.dumps({'status': 'cancelled', 'answers': {}}, ensure_ascii=False)
return JSONCodec.dumps(
{
'status': 'answered',
'answers': output.get('answers', {}),
},
ensure_ascii=False,
)
except Exception as e:
log.exception(f'ask_user error: {e}')
return JSONCodec.dumps({'status': 'error', 'error': str(e)}, ensure_ascii=False)
# =============================================================================
# CODE INTERPRETER TOOLS
# =============================================================================
+4
View File
@@ -47,6 +47,7 @@ from open_webui.models.tools import Tools
from open_webui.models.users import UserModel
from open_webui.tools.builtin import (
add_memory,
ask_user,
calculate_timestamp,
create_automation,
create_calendar_event,
@@ -583,6 +584,9 @@ async def get_builtin_tools(
if is_builtin_tool_enabled('time'):
builtin_functions.extend([get_current_timestamp, calculate_timestamp])
if is_builtin_tool_enabled('user_input'):
builtin_functions.append(ask_user)
metadata = extra_params.get('__metadata__') or {}
chat_files = metadata.get('files') or extra_params.get('__files__') or []
has_chat_files = any(