Files
Timothy Jaeryang Baek f0bfcd4097 refac
2026-08-11 01:15:05 -06:00

661 lines
24 KiB
Python

import logging
import re
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
from fastapi.responses import JSONResponse, RedirectResponse
from open_webui.config import (
DEFAULT_AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE,
DEFAULT_EMOJI_GENERATION_PROMPT_TEMPLATE,
DEFAULT_FOLLOW_UP_GENERATION_PROMPT_TEMPLATE,
DEFAULT_IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,
DEFAULT_MOA_GENERATION_PROMPT_TEMPLATE,
DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE,
DEFAULT_TAGS_GENERATION_PROMPT_TEMPLATE,
DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE,
DEFAULT_VOICE_MODE_PROMPT_TEMPLATE,
)
from open_webui.constants import ERROR_MESSAGES, TASKS
from open_webui.models.config import Config
from open_webui.routers.pipelines import process_pipeline_inlet_filter
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.chat import generate_chat_completion
from open_webui.utils.payload import apply_params_to_form_data
from open_webui.utils.task import (
autocomplete_generation_template,
emoji_generation_template,
follow_up_generation_template,
get_task_model_id,
image_prompt_generation_template,
moa_response_generation_template,
query_generation_template,
tags_generation_template,
title_generation_template,
)
from pydantic import BaseModel
log = logging.getLogger(__name__)
router = APIRouter()
TASK_CONFIG_KEYS = {
'TASK_MODEL': 'task.model.default',
'TASK_MODEL_EXTERNAL': 'task.model.external',
'TASK_MODEL_PARAMS': 'task.model.params',
'TITLE_GENERATION_PROMPT_TEMPLATE': 'task.title.prompt_template',
'IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE': 'task.image.prompt_template',
'ENABLE_AUTOCOMPLETE_GENERATION': 'task.autocomplete.enable',
'AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH': 'task.autocomplete.input_max_length',
'AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE': 'task.autocomplete.prompt_template',
'TAGS_GENERATION_PROMPT_TEMPLATE': 'task.tags.prompt_template',
'FOLLOW_UP_GENERATION_PROMPT_TEMPLATE': 'task.follow_up.prompt_template',
'ENABLE_FOLLOW_UP_GENERATION': 'task.follow_up.enable',
'ENABLE_TAGS_GENERATION': 'task.tags.enable',
'ENABLE_TITLE_GENERATION': 'task.title.enable',
'ENABLE_SEARCH_QUERY_GENERATION': 'task.query.search.enable',
'ENABLE_RETRIEVAL_QUERY_GENERATION': 'task.query.retrieval.enable',
'QUERY_GENERATION_PROMPT_TEMPLATE': 'task.query.prompt_template',
'TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE': 'task.tools.prompt_template',
'ENABLE_VOICE_MODE_PROMPT': 'task.voice.prompt.enable',
'VOICE_MODE_PROMPT_TEMPLATE': 'task.voice.prompt_template',
}
async def get_config_values(key_map: dict[str, str]) -> dict:
values = await Config.get_many(*key_map.values())
return {field: values[storage_key] for field, storage_key in key_map.items() if storage_key in values}
def config_updates(data: dict, key_map: dict[str, str]) -> dict:
return {key_map[field]: value for field, value in data.items() if field in key_map}
def apply_task_model_params(payload: dict, models: dict, task_model_id: str, params: dict | None = None) -> dict:
model = models.get(payload.get('model')) or models.get(task_model_id)
if not model or (not params and not payload.get('params')):
return payload
return apply_params_to_form_data(payload, model, params or None)
async def get_task_model_generation_config(default_model_id: str, models) -> tuple[str, dict]:
config = await Config.get_many(
'task.model.default',
'task.model.external',
'task.model.params',
)
params = config.get('task.model.params') or {}
if not isinstance(params, dict):
params = {}
return (
get_task_model_id(
default_model_id,
config.get('task.model.default'),
config.get('task.model.external'),
models,
),
{key: value for key, value in params.items() if value is not None and value != ''},
)
##################################
#
# Task Endpoints
#
##################################
@router.get('/config')
async def get_task_config(request: Request, user=Depends(get_verified_user)):
return await get_config_values(TASK_CONFIG_KEYS)
class TaskConfigForm(BaseModel):
TASK_MODEL: Optional[str]
TASK_MODEL_EXTERNAL: Optional[str]
TASK_MODEL_PARAMS: dict | None = None
ENABLE_TITLE_GENERATION: bool
TITLE_GENERATION_PROMPT_TEMPLATE: str
IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE: str
ENABLE_AUTOCOMPLETE_GENERATION: bool
AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH: int
AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE: str
TAGS_GENERATION_PROMPT_TEMPLATE: str
FOLLOW_UP_GENERATION_PROMPT_TEMPLATE: str
ENABLE_FOLLOW_UP_GENERATION: bool
ENABLE_TAGS_GENERATION: bool
ENABLE_SEARCH_QUERY_GENERATION: bool
ENABLE_RETRIEVAL_QUERY_GENERATION: bool
QUERY_GENERATION_PROMPT_TEMPLATE: str
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE: str
ENABLE_VOICE_MODE_PROMPT: bool
VOICE_MODE_PROMPT_TEMPLATE: Optional[str]
@router.post('/config/update')
async def update_task_config(request: Request, form_data: TaskConfigForm, user=Depends(get_admin_user)):
await Config.upsert(config_updates(form_data.model_dump(), TASK_CONFIG_KEYS))
return await get_config_values(TASK_CONFIG_KEYS)
@router.post('/title/completions')
async def generate_title(request: Request, form_data: dict, user=Depends(get_verified_user)):
if not await Config.get('task.title.enable'):
return JSONResponse(
status_code=status.HTTP_200_OK,
content={'detail': 'Title generation is disabled'},
)
if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'):
models = {
**dict(request.app.state.MODELS.items()),
request.state.model['id']: request.state.model,
}
else:
models = request.app.state.MODELS
model_id = form_data['model']
if not model_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='No model specified for title generation. Please ensure a model is selected for this chat.',
)
if model_id not in models:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(),
)
task_model_id, task_model_params = await get_task_model_generation_config(model_id, models)
log.debug('generating chat title using model %s for user %s ', task_model_id, user.email)
title_template = await Config.get('task.title.prompt_template')
if title_template != '':
template = title_template
else:
template = DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE
content = await title_generation_template(template, form_data['messages'], user)
task_model_params = task_model_params or {
'max_tokens': models[task_model_id].get('info', {}).get('params', {}).get('max_tokens', 1000)
}
payload = {
'model': task_model_id,
'messages': [{'role': 'user', 'content': content}],
'stream': False,
'metadata': {
**(request.state.metadata if hasattr(request.state, 'metadata') else {}),
'task': str(TASKS.TITLE_GENERATION),
'task_body': form_data,
'chat_id': form_data.get('chat_id', None),
},
}
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
payload = apply_task_model_params(payload, models, task_model_id, task_model_params)
try:
return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e:
log.error('Exception occurred', exc_info=True)
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={'detail': 'An internal error has occurred.'},
)
@router.post('/follow_up/completions')
async def generate_follow_ups(request: Request, form_data: dict, user=Depends(get_verified_user)):
if not await Config.get('task.follow_up.enable'):
return JSONResponse(
status_code=status.HTTP_200_OK,
content={'detail': 'Follow-up generation is disabled'},
)
if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'):
models = {
**dict(request.app.state.MODELS.items()),
request.state.model['id']: request.state.model,
}
else:
models = request.app.state.MODELS
model_id = form_data['model']
if model_id not in models:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(),
)
task_model_id, task_model_params = await get_task_model_generation_config(model_id, models)
log.debug('generating chat title using model %s for user %s ', task_model_id, user.email)
follow_up_template = await Config.get('task.follow_up.prompt_template')
if follow_up_template != '':
template = follow_up_template
else:
template = DEFAULT_FOLLOW_UP_GENERATION_PROMPT_TEMPLATE
content = await follow_up_generation_template(template, form_data['messages'], user)
payload = {
'model': task_model_id,
'messages': [{'role': 'user', 'content': content}],
'stream': False,
'metadata': {
**(request.state.metadata if hasattr(request.state, 'metadata') else {}),
'task': str(TASKS.FOLLOW_UP_GENERATION),
'task_body': form_data,
'chat_id': form_data.get('chat_id', None),
},
}
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
payload = apply_task_model_params(payload, models, task_model_id, task_model_params)
try:
return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e:
log.error('Exception occurred', exc_info=True)
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={'detail': 'An internal error has occurred.'},
)
@router.post('/tags/completions')
async def generate_chat_tags(request: Request, form_data: dict, user=Depends(get_verified_user)):
if not await Config.get('task.tags.enable'):
return JSONResponse(
status_code=status.HTTP_200_OK,
content={'detail': 'Tags generation is disabled'},
)
if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'):
models = {
**dict(request.app.state.MODELS.items()),
request.state.model['id']: request.state.model,
}
else:
models = request.app.state.MODELS
model_id = form_data['model']
if model_id not in models:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(),
)
task_model_id, task_model_params = await get_task_model_generation_config(model_id, models)
log.debug('generating chat tags using model %s for user %s ', task_model_id, user.email)
tags_template = await Config.get('task.tags.prompt_template')
if tags_template != '':
template = tags_template
else:
template = DEFAULT_TAGS_GENERATION_PROMPT_TEMPLATE
content = await tags_generation_template(template, form_data['messages'], user)
payload = {
'model': task_model_id,
'messages': [{'role': 'user', 'content': content}],
'stream': False,
'metadata': {
**(request.state.metadata if hasattr(request.state, 'metadata') else {}),
'task': str(TASKS.TAGS_GENERATION),
'task_body': form_data,
'chat_id': form_data.get('chat_id', None),
},
}
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
payload = apply_task_model_params(payload, models, task_model_id, task_model_params)
try:
return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e:
log.error(f'Error generating chat completion: {e}')
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={'detail': 'An internal error has occurred.'},
)
@router.post('/image_prompt/completions')
async def generate_image_prompt(request: Request, form_data: dict, user=Depends(get_verified_user)):
if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'):
models = {
**dict(request.app.state.MODELS.items()),
request.state.model['id']: request.state.model,
}
else:
models = request.app.state.MODELS
model_id = form_data['model']
if model_id not in models:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(),
)
task_model_id, task_model_params = await get_task_model_generation_config(model_id, models)
log.debug('generating image prompt using model %s for user %s ', task_model_id, user.email)
image_prompt_template = await Config.get('task.image.prompt_template')
if image_prompt_template != '':
template = image_prompt_template
else:
template = DEFAULT_IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE
content = await image_prompt_generation_template(template, form_data['messages'], user)
payload = {
'model': task_model_id,
'messages': [{'role': 'user', 'content': content}],
'stream': False,
'metadata': {
**(request.state.metadata if hasattr(request.state, 'metadata') else {}),
'task': str(TASKS.IMAGE_PROMPT_GENERATION),
'task_body': form_data,
'chat_id': form_data.get('chat_id', None),
},
}
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
payload = apply_task_model_params(payload, models, task_model_id, task_model_params)
try:
return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e:
log.error('Exception occurred', exc_info=True)
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={'detail': 'An internal error has occurred.'},
)
@router.post('/queries/completions')
async def generate_queries(request: Request, form_data: dict, user=Depends(get_verified_user)):
type = form_data.get('type')
if type == 'web_search':
if not await Config.get('task.query.search.enable'):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.FEATURE_DISABLED('Search query generation'),
)
elif type == 'retrieval':
if not await Config.get('task.query.retrieval.enable'):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.FEATURE_DISABLED('Query generation'),
)
if getattr(request.state, 'cached_queries', None):
log.info('Reusing cached queries: %s', request.state.cached_queries)
return request.state.cached_queries
if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'):
models = {
**dict(request.app.state.MODELS.items()),
request.state.model['id']: request.state.model,
}
else:
models = request.app.state.MODELS
model_id = form_data['model']
if model_id not in models:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(),
)
task_model_id, task_model_params = await get_task_model_generation_config(model_id, models)
log.debug('generating %s queries using model %s for user %s', type, task_model_id, user.email)
query_template = await Config.get('task.query.prompt_template')
if query_template.strip() != '':
template = query_template
else:
template = DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE
content = await query_generation_template(template, form_data['messages'], user)
payload = {
'model': task_model_id,
'messages': [{'role': 'user', 'content': content}],
'stream': False,
'metadata': {
**(request.state.metadata if hasattr(request.state, 'metadata') else {}),
'task': str(TASKS.QUERY_GENERATION),
'task_body': form_data,
'chat_id': form_data.get('chat_id', None),
},
}
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
payload = apply_task_model_params(payload, models, task_model_id, task_model_params)
try:
return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={'detail': str(e)},
)
@router.post('/auto/completions')
async def generate_autocompletion(request: Request, form_data: dict, user=Depends(get_verified_user)):
if not await Config.get('task.autocomplete.enable'):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.FEATURE_DISABLED('Autocompletion generation'),
)
type = form_data.get('type')
prompt = form_data.get('prompt')
messages = form_data.get('messages')
autocomplete_input_max_length = await Config.get('task.autocomplete.input_max_length')
if autocomplete_input_max_length > 0:
if len(prompt) > autocomplete_input_max_length:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.INPUT_TOO_LONG(autocomplete_input_max_length),
)
if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'):
models = {
**dict(request.app.state.MODELS.items()),
request.state.model['id']: request.state.model,
}
else:
models = request.app.state.MODELS
model_id = form_data['model']
if model_id not in models:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(),
)
task_model_id, task_model_params = await get_task_model_generation_config(model_id, models)
log.debug('generating autocompletion using model %s for user %s', task_model_id, user.email)
autocomplete_template = await Config.get('task.autocomplete.prompt_template')
if autocomplete_template.strip() != '':
template = autocomplete_template
else:
template = DEFAULT_AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE
content = await autocomplete_generation_template(template, prompt, messages, type, user)
payload = {
'model': task_model_id,
'messages': [{'role': 'user', 'content': content}],
'stream': False,
'metadata': {
**(request.state.metadata if hasattr(request.state, 'metadata') else {}),
'task': str(TASKS.AUTOCOMPLETE_GENERATION),
'task_body': form_data,
'chat_id': form_data.get('chat_id', None),
},
}
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
payload = apply_task_model_params(payload, models, task_model_id, task_model_params)
try:
return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e:
log.error(f'Error generating chat completion: {e}')
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={'detail': 'An internal error has occurred.'},
)
@router.post('/emoji/completions')
async def generate_emoji(request: Request, form_data: dict, user=Depends(get_verified_user)):
if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'):
models = {
**dict(request.app.state.MODELS.items()),
request.state.model['id']: request.state.model,
}
else:
models = request.app.state.MODELS
model_id = form_data['model']
if model_id not in models:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(),
)
task_model_id, _ = await get_task_model_generation_config(model_id, models)
log.debug('generating emoji using model %s for user %s ', task_model_id, user.email)
template = DEFAULT_EMOJI_GENERATION_PROMPT_TEMPLATE
content = await emoji_generation_template(template, form_data['prompt'], user)
payload = {
'model': task_model_id,
'messages': [{'role': 'user', 'content': content}],
'stream': False,
'metadata': {
**(request.state.metadata if hasattr(request.state, 'metadata') else {}),
'task': str(TASKS.EMOJI_GENERATION),
'task_body': form_data,
'chat_id': form_data.get('chat_id', None),
},
}
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
payload = apply_task_model_params(payload, models, task_model_id, {'max_tokens': 4})
try:
return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={'detail': str(e)},
)
@router.post('/moa/completions')
async def generate_moa_response(request: Request, form_data: dict, user=Depends(get_verified_user)):
if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'):
models = {
**dict(request.app.state.MODELS.items()),
request.state.model['id']: request.state.model,
}
else:
models = request.app.state.MODELS
model_id = form_data['model']
if model_id not in models:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(),
)
template = DEFAULT_MOA_GENERATION_PROMPT_TEMPLATE
content = moa_response_generation_template(
template,
form_data['prompt'],
form_data['responses'],
)
payload = {
'model': model_id,
'messages': [{'role': 'user', 'content': content}],
'stream': form_data.get('stream', False),
'metadata': {
**(request.state.metadata if hasattr(request.state, 'metadata') else {}),
'chat_id': form_data.get('chat_id', None),
'task': str(TASKS.MOA_RESPONSE_GENERATION),
'task_body': form_data,
},
}
# Process the payload through the pipeline
try:
payload = await process_pipeline_inlet_filter(request, payload, user, models)
except Exception as e:
raise e
try:
return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={'detail': str(e)},
)