|
98 | 98 | convert_logit_bias_input_to_json, |
99 | 99 | get_content_from_message, |
100 | 100 | convert_output_to_messages, |
| 101 | + filter_output_by_content, |
101 | 102 | strip_empty_content_blocks, |
102 | 103 | ) |
103 | 104 | from open_webui.utils.tools import ( |
@@ -481,9 +482,9 @@ def serialize_output(output: list) -> str: |
481 | 482 | ) |
482 | 483 |
|
483 | 484 | if status == 'completed' or duration is not None or not is_last_item: |
484 | | - content = f'{content}<details type="reasoning" done="true" duration="{duration or 0}">\n<summary>Thought for {duration or 0} seconds</summary>\n{display}\n</details>\n' |
| 485 | + content = f'{content}<details type="reasoning" done="true" id="{item.get("id", "")}" duration="{duration or 0}">\n<summary>Thought for {duration or 0} seconds</summary>\n{display}\n</details>\n' |
485 | 486 | else: |
486 | | - content = f'{content}<details type="reasoning" done="false">\n<summary>Thinking…</summary>\n{display}\n</details>\n' |
| 487 | + content = f'{content}<details type="reasoning" done="false" id="{item.get("id", "")}">\n<summary>Thinking…</summary>\n{display}\n</details>\n' |
487 | 488 |
|
488 | 489 | elif item_type == 'open_webui:code_interpreter': |
489 | 490 | content_stripped, original_whitespace = split_content_and_whitespace(content) |
@@ -519,9 +520,9 @@ def serialize_output(output: list) -> str: |
519 | 520 | output_attr = f' output="{html.escape(output_json)}"' |
520 | 521 |
|
521 | 522 | if status == 'completed' or duration is not None or not is_last_item: |
522 | | - content += f'<details type="code_interpreter" done="true" duration="{duration or 0}"{output_attr}>\n<summary>Analyzed</summary>\n{display}\n</details>\n' |
| 523 | + content += f'<details type="code_interpreter" done="true" id="{item.get("id", "")}" duration="{duration or 0}"{output_attr}>\n<summary>Analyzed</summary>\n{display}\n</details>\n' |
523 | 524 | else: |
524 | | - content += f'<details type="code_interpreter" done="false"{output_attr}>\n<summary>Analyzing…</summary>\n{display}\n</details>\n' |
| 525 | + content += f'<details type="code_interpreter" done="false" id="{item.get("id", "")}"{output_attr}>\n<summary>Analyzing…</summary>\n{display}\n</details>\n' |
525 | 526 |
|
526 | 527 | return content.strip() |
527 | 528 |
|
@@ -2092,16 +2093,47 @@ def process_messages_with_output(messages: list[dict]) -> list[dict]: |
2092 | 2093 |
|
2093 | 2094 | For assistant messages with 'output' field, produces properly formatted |
2094 | 2095 | OpenAI-style messages (tool_calls + tool results). Strips 'output' before LLM. |
| 2096 | + Respects content edits and dropped <details> blocks by filtering output items |
| 2097 | + against the stored content field before conversion. |
2095 | 2098 | """ |
2096 | 2099 | processed = [] |
2097 | 2100 |
|
2098 | 2101 | for message in messages: |
2099 | 2102 | if message.get('role') == 'assistant' and message.get('output'): |
2100 | | - # Use output items for clean OpenAI-format messages |
2101 | | - output_messages = convert_output_to_messages(message['output'], raw=True) |
| 2103 | + # Drop output items for <details> blocks removed from content |
| 2104 | + filtered_output = filter_output_by_content( |
| 2105 | + message['output'], message.get('content', '') |
| 2106 | + ) |
| 2107 | + |
| 2108 | + # Use content for text (respects edits), strip <details> blocks |
| 2109 | + edited_text = re.sub( |
| 2110 | + r'<details\b[^>]*>.*?</details>', '', |
| 2111 | + message.get('content', ''), flags=re.S, |
| 2112 | + ).strip() |
| 2113 | + |
| 2114 | + # Replace the first message item's text with the edited content, |
| 2115 | + # preserving the natural order of structured items (reasoning before |
| 2116 | + # text, tool calls in sequence) via convert_output_to_messages. |
| 2117 | + used_edited_text = False |
| 2118 | + modified_output = [] |
| 2119 | + for item in filtered_output: |
| 2120 | + if item.get('type') == 'message' and not used_edited_text: |
| 2121 | + modified_output.append({ |
| 2122 | + **item, |
| 2123 | + 'content': [{'type': 'output_text', 'text': edited_text}], |
| 2124 | + }) |
| 2125 | + used_edited_text = True |
| 2126 | + else: |
| 2127 | + modified_output.append(item) |
| 2128 | + |
| 2129 | + output_messages = convert_output_to_messages(modified_output, raw=True) |
2102 | 2130 | if output_messages: |
| 2131 | + if not used_edited_text and edited_text: |
| 2132 | + output_messages.append({'role': 'assistant', 'content': edited_text}) |
2103 | 2133 | processed.extend(output_messages) |
2104 | | - continue |
| 2134 | + elif edited_text: |
| 2135 | + processed.append({'role': 'assistant', 'content': edited_text}) |
| 2136 | + continue |
2105 | 2137 |
|
2106 | 2138 | # Strip 'output' field before adding (LLM shouldn't see it) |
2107 | 2139 | clean_message = {k: v for k, v in message.items() if k != 'output'} |
|
0 commit comments