From ecbbff8afb3537d5e799e434a61a1134a97c6362 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 9 Oct 2026 10:22:37 +0400 Subject: [PATCH] refac --- backend/open_webui/main.py | 12 +++- backend/open_webui/utils/middleware.py | 79 ++++++++++++-------------- backend/open_webui/utils/subagents.py | 39 +++++++------ backend/open_webui/utils/timers.py | 11 +++- 4 files changed, 79 insertions(+), 62 deletions(-) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 0ed3d753ea..96a85be50e 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1315,6 +1315,15 @@ async def chat_completion( 'features': form_data.get('features', {}), 'variables': form_data.get('variables', {}), 'chat_variables': chat_variables, + # Later requests rebuild generated instructions from this original chat context. + 'chat_context': { + **copy.deepcopy(getattr(request.state, 'chat_context', None) or {}), + 'messages': copy.deepcopy( + [message for message in form_data.get('messages', []) if message.get('role') == 'system'] + ), + 'params': {'system': request_params['system']} if 'system' in request_params else {}, + 'chat_variables': copy.deepcopy(chat_variables), + }, 'model': model, 'direct': model_item.get('direct', False), 'params': { @@ -1762,7 +1771,7 @@ async def chat_completion( 'session_id': metadata.get('session_id'), 'tool_ids': metadata.get('tool_ids') or [], 'skill_ids': metadata.get('skill_ids') or [], - 'system_prompt': metadata.get('system_prompt'), + 'chat_context': metadata.get('chat_context'), 'filter_ids': metadata.get('filter_ids') or [], 'terminal_id': metadata.get('terminal_id'), 'features': metadata.get('features') or {}, @@ -1791,6 +1800,7 @@ async def chat_completion( # Per-model metadata: own message_id + model per_model_metadata = { **metadata, + 'chat_context': copy.deepcopy(metadata['chat_context']), 'message_id': assistant_message_id, 'task_id': str(uuid4()), } diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 339c03a485..afdce7c9bd 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -2399,8 +2399,8 @@ async def process_chat_payload(request, form_data, user, metadata, model): form_data['model'] = selected_model_id metadata['selected_model_id'] = selected_model_id - # Captured before apply_params_to_form_data pops 'params'; populates metadata['system_prompt'] below - model_system_prompt = (form_data.get('params') or {}).get('system') + # Keep the template before apply_params_to_form_data consumes the model parameters. + model_system_prompt_template = (form_data.get('params') or {}).get('system') form_data = apply_params_to_form_data(form_data, model) log.debug('form_data: %s', form_data) @@ -2487,7 +2487,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): compaction_models = request.app.state.MODELS system_message = get_system_message(form_data.get('messages', [])) - system_prompt = get_content_from_message(system_message) if system_message else '' + chat_system_prompt = get_content_from_message(system_message) if system_message else '' try: form_data['messages'], context_summary, _ = await compact_messages_for_request( @@ -2497,7 +2497,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): metadata, form_data.get('model'), compaction_models, - system_prompt, + chat_system_prompt, ) if context_summary: form_data['messages'] = add_or_update_system_message( @@ -2591,8 +2591,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): folder = None if folder and folder.data: - # A sub-agent already gets it in the parent's system prompt - if 'system_prompt' in folder.data and not metadata.get('internal'): + if 'system_prompt' in folder.data: form_data = await apply_system_prompt_to_body(folder.data['system_prompt'], form_data, metadata, user) if 'files' in folder.data: if metadata.get('params', {}).get('function_calling') == 'legacy': @@ -2776,7 +2775,10 @@ async def process_chat_payload(request, form_data, user, metadata, model): inlet_filter_tools = None if payload_tools is not None else form_data.get('tools', None) # Mentioned skills get full content; selected/default skills can be loaded through view_skill. - mentioned_skill_ids = extract_skill_ids_from_messages(form_data.get('messages', [])) + chat_context = metadata.get('chat_context') or {} + mentioned_skill_ids = set(chat_context.get('mentioned_skill_ids') or []) | extract_skill_ids_from_messages( + form_data.get('messages', []) + ) skill_ids = sorted( set(form_data.pop('skill_ids', None) or []) | set(model.get('info', {}).get('meta', {}).get('skillIds', [])) @@ -2887,6 +2889,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): # Strip only resolved skill mentions; ordinary text such as Perl's <$fh> stays intact. resolved_skill_ids = {s.id for s in available_skills} | {s['id'] for s in terminal_skills} + chat_context['mentioned_skill_ids'] = sorted(mentioned_skill_ids & resolved_skill_ids) strip_skill_mentions(form_data.get('messages', []), resolved_skill_ids) prompt = get_last_user_message(form_data['messages']) @@ -3036,15 +3039,15 @@ async def process_chat_payload(request, form_data, user, metadata, model): extra_params, ) if isinstance(terminal_result, tuple): - terminal_tools, system_prompt = terminal_result + terminal_tools, terminal_system_prompt = terminal_result else: terminal_tools = terminal_result - system_prompt = None + terminal_system_prompt = None if terminal_tools: tools_dict = {**tools_dict, **terminal_tools} - if system_prompt: + if terminal_system_prompt: form_data['messages'] = add_or_update_system_message( - system_prompt, + terminal_system_prompt, form_data['messages'], append=True, ) @@ -3058,10 +3061,10 @@ async def process_chat_payload(request, form_data, user, metadata, model): continue # Copy so the pops below keep metadata intact for sub-agents and approval resumes tool_server = dict(tool_server) - system_prompt = tool_server.pop('system_prompt', None) - if system_prompt: + tool_server_system_prompt = tool_server.pop('system_prompt', None) + if tool_server_system_prompt: form_data['messages'] = add_or_update_system_message( - system_prompt, + tool_server_system_prompt, form_data['messages'], append=True, ) @@ -3213,22 +3216,22 @@ async def process_chat_payload(request, form_data, user, metadata, model): except Exception as e: log.exception(e) - # Save the pre-RAG message state so the native tool call loop can - # restore to the true original (before file-source injection) rather - # than a snapshot that already has the RAG template baked in. + # The tool loop restores these prompts before rebuilding file and tool source context. system_message = get_system_message(form_data['messages']) - system_content = get_content_from_message(system_message) if system_message else '' + base_system_prompt = get_content_from_message(system_message) if system_message else '' resolved_model_system_prompt = await resolve_system_prompt( - model_system_prompt, + model_system_prompt_template, metadata, user, ) if resolved_model_system_prompt: - system_content = ( - f'{resolved_model_system_prompt}\n{system_content}' if system_content else resolved_model_system_prompt + base_system_prompt = ( + f'{resolved_model_system_prompt}\n{base_system_prompt}' + if base_system_prompt + else resolved_model_system_prompt ) - metadata['system_prompt'] = system_content or None - metadata['user_prompt'] = get_last_user_message(form_data['messages']) + metadata['base_system_prompt'] = base_system_prompt or None + metadata['base_user_prompt'] = get_last_user_message(form_data['messages']) metadata['sources'] = sources[:] if sources else [] # If context is not empty, insert it into the messages @@ -5821,15 +5824,11 @@ async def streaming_chat_response_handler(response, ctx): 'citations', True ) - # Use the pre-RAG system content captured before the - # initial file-source injection in process_chat_payload. - # This ensures restore truly undoes the RAG template. - original_system_content = metadata.get('system_prompt') - if original_system_content is None: - original_system_message = get_system_message(form_data['messages']) - original_system_content = ( - get_content_from_message(original_system_message) if original_system_message else None - ) + # Requests that bypass payload processing have no saved source-free prompt. + base_system_prompt = metadata.get('base_system_prompt') + if base_system_prompt is None: + system_message = get_system_message(form_data['messages']) + base_system_prompt = get_content_from_message(system_message) if system_message else None async def emit_output(): # Channels publish whole messages; Continue can merge into the preceding item. @@ -6083,27 +6082,23 @@ async def streaming_chat_response_handler(response, ctx): for source in tool_call_sources: await event_emitter({'type': 'source', 'data': source}) - # Apply tool source context to messages for the model. - # Restoring to pre-RAG original prevents duplicating - # the RAG template across file and tool sources. + # Rebuild source context from the saved prompts so it never accumulates. all_tool_call_sources.extend(tool_call_sources) if all_tool_call_sources and user_message: - # Restore pre-RAG message state before re-applying - # to prevent RAG template duplication. - original_user_message = metadata.get('user_prompt') or user_message + base_user_prompt = metadata.get('base_user_prompt') or user_message set_last_user_message_content( - original_user_message, + base_user_prompt, form_data['messages'], ) - if original_system_content is not None: + if base_system_prompt is not None: if get_system_message(form_data['messages']): replace_system_message_content( - original_system_content, + base_system_prompt, form_data['messages'], ) else: form_data['messages'] = add_or_update_system_message( - original_system_content, + base_system_prompt, form_data['messages'], ) else: diff --git a/backend/open_webui/utils/subagents.py b/backend/open_webui/utils/subagents.py index c59a068c1c..916c078cc0 100644 --- a/backend/open_webui/utils/subagents.py +++ b/backend/open_webui/utils/subagents.py @@ -18,7 +18,7 @@ from open_webui.models.users import UserModel, Users from open_webui.tasks import create_task, has_active_tasks from open_webui.utils.auth import VERIFIED_USER_ROLES, create_token from open_webui.utils.json_codec import JSONCodec -from open_webui.utils.misc import get_message_list +from open_webui.utils.misc import add_or_update_system_message, get_message_list from sqlalchemy import select from starlette.datastructures import Headers @@ -127,7 +127,9 @@ async def process_pending_internal_messages( parent_id = first.get('parentId') if kind == 'timer' and first_meta.get('timer_id'): timer = await Chats.get_chat_by_id(first_meta['timer_id']) - run = {**run, **(((timer.meta or {}).get('run') if timer else None) or {})} + timer_run = ((timer.meta or {}).get('run') if timer else None) or {} + if timer_run: + run = {**run, **timer_run, 'chat_context': timer_run.get('chat_context')} model_id = first.get('model') or run['model_id'] if kind == 'timer': batch = [first] @@ -191,7 +193,6 @@ async def process_pending_internal_messages( assistant_message_id = str(uuid4()) message_list = get_message_list(messages, parent_id) - system_prompt = run.get('system_prompt') user_message = { 'id': user_message_id, 'parentId': parent_id, @@ -245,10 +246,15 @@ async def process_pending_internal_messages( room=f'user:{user.id}', ) + chat_context = copy.deepcopy(run.get('chat_context') or {}) + # Runs saved before chat_context was introduced only have the assembled prompt. + if run.get('chat_context') is None and run.get('system_prompt'): + chat_context['messages'] = [{'role': 'system', 'content': run['system_prompt']}] form_data = { 'model': model_id, + 'params': chat_context.get('params') or {}, 'messages': [ - *([{'role': 'system', 'content': system_prompt}] if system_prompt else []), + *chat_context.get('messages', []), *message_list, {'role': 'user', 'content': combined_content}, ], @@ -265,11 +271,13 @@ async def process_pending_internal_messages( 'features': run.get('features') or {}, 'files': run.get('files') or [], 'variables': run.get('variables') or {}, + 'chat_variables': chat_context.get('chat_variables'), } if run.get('terminal_id'): form_data['terminal_id'] = run['terminal_id'] request = await _build_request(source_request, user.id, internal=False) + request.state.chat_context = chat_context await source_request.app.state.CHAT_COMPLETION_HANDLER(request, form_data, user=user) @@ -326,7 +334,7 @@ async def delegate( 'session_id': metadata.get('session_id'), 'tool_ids': copy.deepcopy(metadata.get('tool_ids') or []), 'skill_ids': copy.deepcopy(metadata.get('skill_ids') or []), - 'system_prompt': metadata.get('system_prompt'), + 'chat_context': copy.deepcopy(metadata.get('chat_context') or {}), 'tool_servers': [] if background else copy.deepcopy(metadata.get('tool_servers') or []), 'filter_ids': copy.deepcopy(metadata.get('filter_ids') or []), 'terminal_id': metadata.get('terminal_id'), @@ -457,23 +465,19 @@ async def delegate( try: child_request = await _build_request(request, user.id, internal=True) child_request.state.max_tool_call_iterations = max_iterations - parent_system_prompt = run.get('system_prompt') or '' + chat_context = copy.deepcopy(run.get('chat_context') or {}) + child_request.state.chat_context = chat_context subagent_system_prompt = ( str(config.get('subagents.system_prompt') or '').strip() or DEFAULT_SUBAGENT_SYSTEM_PROMPT ) form_data = { 'model': run['model_id'], - 'messages': [ - { - 'role': 'system', - 'content': ( - f'{parent_system_prompt}\n\n{subagent_system_prompt}' - if parent_system_prompt - else subagent_system_prompt - ), - }, - {'role': 'user', 'content': prompt}, - ], + 'params': chat_context.get('params') or {}, + 'messages': add_or_update_system_message( + subagent_system_prompt, + [*chat_context.get('messages', []), {'role': 'user', 'content': prompt}], + append=True, + ), 'stream': True, 'chat_id': chat_id, 'id': assistant_message_id, @@ -487,6 +491,7 @@ async def delegate( 'features': run.get('features') or {}, 'files': run.get('files') or [], 'variables': run.get('variables') or {}, + 'chat_variables': chat_context.get('chat_variables'), 'folder_id': run.get('folder_id'), } if run.get('terminal_id'): diff --git a/backend/open_webui/utils/timers.py b/backend/open_webui/utils/timers.py index c2a91139ef..bfd1829c2f 100644 --- a/backend/open_webui/utils/timers.py +++ b/backend/open_webui/utils/timers.py @@ -108,7 +108,7 @@ async def create_timer( 'session_id': metadata.get('session_id'), 'tool_ids': copy.deepcopy(metadata.get('tool_ids') or []), 'skill_ids': copy.deepcopy(metadata.get('skill_ids') or []), - 'system_prompt': metadata.get('system_prompt'), + 'chat_context': copy.deepcopy(metadata.get('chat_context') or {}), 'filter_ids': copy.deepcopy(metadata.get('filter_ids') or []), 'terminal_id': metadata.get('terminal_id'), 'features': copy.deepcopy(metadata.get('features') or {}), @@ -374,10 +374,15 @@ async def execute_due_timer(app, timer_id: str, claim_id: str | None = None) -> }, room=f'user:{timer.user_id}', ) + chat_context = copy.deepcopy(run.get('chat_context') or {}) + # Timers saved before chat_context was introduced only have the assembled prompt. + if run.get('chat_context') is None and run.get('system_prompt'): + chat_context['messages'] = [{'role': 'system', 'content': run['system_prompt']}] form_data = { 'model': model_id, + 'params': chat_context.get('params') or {}, 'messages': [ - *([{'role': 'system', 'content': run.get('system_prompt')}] if run.get('system_prompt') else []), + *chat_context.get('messages', []), *message_list, {'role': 'user', 'content': prompt}, ], @@ -394,6 +399,7 @@ async def execute_due_timer(app, timer_id: str, claim_id: str | None = None) -> 'features': run.get('features') or {}, 'files': run.get('files') or [], 'variables': run.get('variables') or {}, + 'chat_variables': chat_context.get('chat_variables'), } if run.get('terminal_id'): form_data['terminal_id'] = run['terminal_id'] @@ -413,6 +419,7 @@ async def execute_due_timer(app, timer_id: str, claim_id: str | None = None) -> ) request.state.token = None request.state.enable_api_keys = False + request.state.chat_context = chat_context try: await app.state.CHAT_COMPLETION_HANDLER(request, form_data, user=user) except Exception as exc: