This commit is contained in:
Timothy Jaeryang Baek 2026-10-09 10:22:37 +04:00
parent 893bf2090f
commit ecbbff8afb
4 changed files with 79 additions and 62 deletions

View file

@ -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()),
}

View file

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

View file

@ -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'):

View file

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