|
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, |
102 | 101 | strip_empty_content_blocks, |
103 | 102 | ) |
104 | 103 | from open_webui.utils.tools import ( |
@@ -252,8 +251,6 @@ def get_citation_source_from_tool_result( |
252 | 251 | if tool_name == 'search_web': |
253 | 252 | # Parse JSON array: [{"title": "...", "link": "...", "snippet": "..."}] |
254 | 253 | results = tool_result |
255 | | - if not isinstance(results, list): |
256 | | - return [] |
257 | 254 | documents = [] |
258 | 255 | metadata = [] |
259 | 256 |
|
@@ -484,9 +481,9 @@ def serialize_output(output: list) -> str: |
484 | 481 | ) |
485 | 482 |
|
486 | 483 | if status == 'completed' or duration is not None or not is_last_item: |
487 | | - 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' |
| 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' |
488 | 485 | else: |
489 | | - content = f'{content}<details type="reasoning" done="false" id="{item.get("id", "")}">\n<summary>Thinking…</summary>\n{display}\n</details>\n' |
| 486 | + content = f'{content}<details type="reasoning" done="false">\n<summary>Thinking…</summary>\n{display}\n</details>\n' |
490 | 487 |
|
491 | 488 | elif item_type == 'open_webui:code_interpreter': |
492 | 489 | content_stripped, original_whitespace = split_content_and_whitespace(content) |
@@ -522,9 +519,9 @@ def serialize_output(output: list) -> str: |
522 | 519 | output_attr = f' output="{html.escape(output_json)}"' |
523 | 520 |
|
524 | 521 | if status == 'completed' or duration is not None or not is_last_item: |
525 | | - 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' |
| 522 | + content += f'<details type="code_interpreter" done="true" duration="{duration or 0}"{output_attr}>\n<summary>Analyzed</summary>\n{display}\n</details>\n' |
526 | 523 | else: |
527 | | - content += f'<details type="code_interpreter" done="false" id="{item.get("id", "")}"{output_attr}>\n<summary>Analyzing…</summary>\n{display}\n</details>\n' |
| 524 | + content += f'<details type="code_interpreter" done="false"{output_attr}>\n<summary>Analyzing…</summary>\n{display}\n</details>\n' |
528 | 525 |
|
529 | 526 | return content.strip() |
530 | 527 |
|
@@ -2091,47 +2088,16 @@ def process_messages_with_output(messages: list[dict]) -> list[dict]: |
2091 | 2088 |
|
2092 | 2089 | For assistant messages with 'output' field, produces properly formatted |
2093 | 2090 | OpenAI-style messages (tool_calls + tool results). Strips 'output' before LLM. |
2094 | | - Respects content edits and dropped <details> blocks by filtering output items |
2095 | | - against the stored content field before conversion. |
2096 | 2091 | """ |
2097 | 2092 | processed = [] |
2098 | 2093 |
|
2099 | 2094 | for message in messages: |
2100 | 2095 | if message.get('role') == 'assistant' and message.get('output'): |
2101 | | - # Drop output items for <details> blocks removed from content |
2102 | | - filtered_output = filter_output_by_content( |
2103 | | - message['output'], message.get('content', '') |
2104 | | - ) |
2105 | | - |
2106 | | - # Use content for text (respects edits), strip <details> blocks |
2107 | | - edited_text = re.sub( |
2108 | | - r'<details\b[^>]*>.*?</details>', '', |
2109 | | - message.get('content', ''), flags=re.S, |
2110 | | - ).strip() |
2111 | | - |
2112 | | - # Replace the first message item's text with the edited content, |
2113 | | - # preserving the natural order of structured items (reasoning before |
2114 | | - # text, tool calls in sequence) via convert_output_to_messages. |
2115 | | - used_edited_text = False |
2116 | | - modified_output = [] |
2117 | | - for item in filtered_output: |
2118 | | - if item.get('type') == 'message' and not used_edited_text: |
2119 | | - modified_output.append({ |
2120 | | - **item, |
2121 | | - 'content': [{'type': 'output_text', 'text': edited_text}], |
2122 | | - }) |
2123 | | - used_edited_text = True |
2124 | | - else: |
2125 | | - modified_output.append(item) |
2126 | | - |
2127 | | - output_messages = convert_output_to_messages(modified_output, raw=True) |
| 2096 | + # Use output items for clean OpenAI-format messages |
| 2097 | + output_messages = convert_output_to_messages(message['output'], raw=True) |
2128 | 2098 | if output_messages: |
2129 | | - if not used_edited_text and edited_text: |
2130 | | - output_messages.append({'role': 'assistant', 'content': edited_text}) |
2131 | 2099 | processed.extend(output_messages) |
2132 | | - elif edited_text: |
2133 | | - processed.append({'role': 'assistant', 'content': edited_text}) |
2134 | | - continue |
| 2100 | + continue |
2135 | 2101 |
|
2136 | 2102 | # Strip 'output' field before adding (LLM shouldn't see it) |
2137 | 2103 | clean_message = {k: v for k, v in message.items() if k != 'output'} |
@@ -3309,7 +3275,7 @@ def set_last_text(out, text): |
3309 | 3275 | if start_tag.startswith('<') and start_tag.endswith('>'): |
3310 | 3276 | start_tag_pattern = rf'<{re.escape(start_tag[1:-1])}(\s.*?)?>' |
3311 | 3277 |
|
3312 | | - match = re.search(start_tag_pattern, item_text) |
| 3278 | + match = re.match(rf'[ \t\n]*{start_tag_pattern}', item_text) |
3313 | 3279 | if match: |
3314 | 3280 | try: |
3315 | 3281 | attr_content = match.group(1) if match.group(1) else '' |
@@ -3413,7 +3379,7 @@ def set_last_text(out, text): |
3413 | 3379 | else: |
3414 | 3380 | block_content = get_last_text(output) |
3415 | 3381 |
|
3416 | | - if end_tag in block_content[-512:]: |
| 3382 | + if re.search(end_tag_pattern, block_content): |
3417 | 3383 | end_flag = True |
3418 | 3384 |
|
3419 | 3385 | # Strip start and end tags from content |
|
0 commit comments