From 89b04502ac9d9f27a7c676244aa23338846340e3 Mon Sep 17 00:00:00 2001 From: eniraa Date: Tue, 17 Mar 2026 12:00:59 -0700 Subject: [PATCH] fix message editing --- backend/open_webui/main.py | 1 + backend/open_webui/utils/middleware.py | 15 +++++--- backend/open_webui/utils/misc.py | 47 +++++++++++++++++++++++++- src/lib/components/chat/Chat.svelte | 13 +++++-- 4 files changed, 69 insertions(+), 7 deletions(-) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 18981940ad..6dbb3636af 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1784,6 +1784,7 @@ async def chat_completion( "user_id": user.id, "chat_id": form_data.pop("chat_id", None), "message_id": form_data.pop("id", None), + "context_message_id": form_data.pop("context_message_id", None), "parent_message": form_data.pop("parent_message", None), "parent_message_id": form_data.pop("parent_id", None), "session_id": form_data.pop("session_id", None), diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index f0a79ef4b1..5991737cc8 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -97,6 +97,7 @@ from open_webui.utils.misc import ( convert_logit_bias_input_to_json, get_content_from_message, convert_output_to_messages, + merge_assistant_content_into_output_messages, ) from open_webui.utils.tools import ( get_tools, @@ -2131,7 +2132,11 @@ def process_messages_with_output(messages: list[dict]) -> list[dict]: # Use output items for clean OpenAI-format messages output_messages = convert_output_to_messages(message["output"], raw=True) if output_messages: - processed.extend(output_messages) + processed.extend( + merge_assistant_content_into_output_messages( + output_messages, message.get("content") + ) + ) continue # Strip 'output' field before adding (LLM shouldn't see it) @@ -2152,10 +2157,12 @@ async def process_chat_payload(request, form_data, user, metadata, model): # Load messages from DB when available — DB preserves structured 'output' items # which the frontend strips, causing tool calls to be merged into content. chat_id = metadata.get("chat_id") - parent_message_id = metadata.get("parent_message_id") + context_message_id = metadata.get("context_message_id") or metadata.get( + "parent_message_id" + ) - if chat_id and parent_message_id and not chat_id.startswith("local:"): - db_messages = load_messages_from_db(chat_id, parent_message_id) + if chat_id and context_message_id and not chat_id.startswith("local:"): + db_messages = load_messages_from_db(chat_id, context_message_id) if db_messages: system_message = get_system_message(form_data.get("messages", [])) form_data["messages"] = ( diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index 809a982e6b..f9caf27dfa 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -6,7 +6,7 @@ import uuid import logging from datetime import timedelta from pathlib import Path -from typing import Callable, Optional, Sequence, Union +from typing import Any, Callable, Optional, Sequence, Union import json import aiohttp import mimeparse @@ -275,6 +275,51 @@ def convert_output_to_messages(output: list, raw: bool = False) -> list[dict]: return messages +def merge_assistant_content_into_output_messages( + messages: list[dict], content: Any +) -> list[dict]: + """ + Preserve tool/reasoning reconstruction from stored `output`, but let an + edited assistant `content` replace the final assistant-visible text. + + This keeps the backend-authoritative message chain editable without + discarding tool calls or other structured output items. + """ + if not messages: + return messages + + content_text = get_content_from_message({"content": content}) + if not content_text or not content_text.strip(): + return messages + + merged_messages = list(messages) + assistant_indexes = [ + idx + for idx, message in enumerate(merged_messages) + if message.get("role") == "assistant" + ] + + if not assistant_indexes: + return [*merged_messages, {"role": "assistant", "content": content}] + + non_tool_assistant_indexes = [ + idx + for idx in assistant_indexes + if not merged_messages[idx].get("tool_calls") + ] + target_idx = ( + non_tool_assistant_indexes[-1] + if non_tool_assistant_indexes + else assistant_indexes[-1] + ) + + merged_messages[target_idx] = { + **merged_messages[target_idx], + "content": content, + } + return merged_messages + + def get_last_user_message(messages: list[dict]) -> Optional[str]: message = get_last_user_message_item(messages) if message is None: diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 41b25606a5..3079d4ad57 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -2030,7 +2030,14 @@ .map((token) => decodeURIComponent(JSON.parse(`"${token.replace(/"/g, '\\"')}"`))); }; - const sendMessageSocket = async (model, _messages, _history, responseMessageId, _chatId) => { + const sendMessageSocket = async ( + model, + _messages, + _history, + responseMessageId, + _chatId, + { contextMessageId = null }: { contextMessageId?: string | null } = {} + ) => { const responseMessage = _history.messages[responseMessageId]; const userMessage = _history.messages[responseMessage.parentId]; @@ -2223,6 +2230,7 @@ chat_id: $chatId, id: responseMessageId, + context_message_id: contextMessageId ?? undefined, parent_id: userMessage?.id ?? null, parent_message: userMessage, @@ -2458,7 +2466,8 @@ createMessagesList(history, responseMessage.id), history, responseMessage.id, - _chatId + _chatId, + { contextMessageId: responseMessage.id } ); } }