mirror of
https://github.com/open-webui/open-webui.git
synced 2026-09-08 22:21:38 +00:00
refactor(middleware): optimize fence-stripping O(n²) → O(n)
Replace repeated string concatenation in serialize_output with list-append + single join, eliminating quadratic copying. Inner accumulators (reasoning_content, result_text) receive the same treatment. Fence-stripping for code_interpreter is rewritten to operate on the parts list instead of the monolithic string. Co-authored-by: alifurkanstahl <180474740+alifurkanstahl@users.noreply.github.com>
This commit is contained in:
parent
f6b85700ea
commit
64ff29df4d
1 changed files with 51 additions and 33 deletions
|
|
@ -389,16 +389,15 @@ def get_citation_source_from_tool_result(
|
|||
]
|
||||
|
||||
|
||||
def split_content_and_whitespace(content):
|
||||
content_stripped = content.rstrip()
|
||||
original_whitespace = content[len(content_stripped) :] if len(content) > len(content_stripped) else ''
|
||||
return content_stripped, original_whitespace
|
||||
def content_endswith_newline(parts):
|
||||
if not parts:
|
||||
return False
|
||||
return parts[-1].endswith('\n')
|
||||
|
||||
|
||||
def is_opening_code_block(content):
|
||||
backtick_segments = content.split('```')
|
||||
# Even number of segments means the last backticks are opening a new block
|
||||
return len(backtick_segments) > 1 and len(backtick_segments) % 2 == 0
|
||||
def ensure_trailing_newline(parts):
|
||||
if parts and not content_endswith_newline(parts):
|
||||
parts.append('\n')
|
||||
|
||||
|
||||
def serialize_output(output: list) -> str:
|
||||
|
|
@ -406,7 +405,13 @@ def serialize_output(output: list) -> str:
|
|||
Convert OR-aligned output items to HTML for display.
|
||||
For LLM consumption, use convert_output_to_messages() instead.
|
||||
"""
|
||||
content = ''
|
||||
content_parts = []
|
||||
fence_count = 0
|
||||
|
||||
def append_part(part):
|
||||
nonlocal fence_count
|
||||
content_parts.append(part)
|
||||
fence_count += part.count('```')
|
||||
|
||||
# First pass: collect function_call_output items by call_id for lookup
|
||||
tool_outputs = {}
|
||||
|
|
@ -423,12 +428,12 @@ def serialize_output(output: list) -> str:
|
|||
if 'text' in content_part:
|
||||
text = content_part.get('text', '').strip()
|
||||
if text:
|
||||
content = f'{content}{text}\n'
|
||||
append_part(text)
|
||||
append_part('\n')
|
||||
|
||||
elif item_type == 'function_call':
|
||||
# Render tool call inline with its result (if available)
|
||||
if content and not content.endswith('\n'):
|
||||
content += '\n'
|
||||
ensure_trailing_newline(content_parts)
|
||||
|
||||
call_id = item.get('call_id', '')
|
||||
name = item.get('name', '')
|
||||
|
|
@ -436,33 +441,34 @@ def serialize_output(output: list) -> str:
|
|||
|
||||
result_item = tool_outputs.get(call_id)
|
||||
if result_item:
|
||||
result_text = ''
|
||||
result_text_parts = []
|
||||
for result_output in result_item.get('output', []):
|
||||
if 'text' in result_output:
|
||||
output_text = result_output.get('text', '')
|
||||
result_text += str(output_text) if not isinstance(output_text, str) else output_text
|
||||
result_text_parts.append(str(output_text))
|
||||
result_text = ''.join(result_text_parts)
|
||||
files = result_item.get('files')
|
||||
embeds = result_item.get('embeds', '')
|
||||
|
||||
content += f'<details type="tool_calls" done="true" id="{call_id}" name="{name}" arguments="{html.escape(json.dumps(arguments))}" result="{html.escape(json.dumps(result_text, ensure_ascii=False))}" files="{html.escape(json.dumps(files)) if files else ""}" embeds="{html.escape(json.dumps(embeds))}">\n<summary>Tool Executed</summary>\n</details>\n'
|
||||
append_part(f'<details type="tool_calls" done="true" id="{call_id}" name="{name}" arguments="{html.escape(json.dumps(arguments))}" result="{html.escape(json.dumps(result_text, ensure_ascii=False))}" files="{html.escape(json.dumps(files)) if files else ""}" embeds="{html.escape(json.dumps(embeds))}">\n<summary>Tool Executed</summary>\n</details>\n')
|
||||
else:
|
||||
content += f'<details type="tool_calls" done="false" id="{call_id}" name="{name}" arguments="{html.escape(json.dumps(arguments))}">\n<summary>Executing...</summary>\n</details>\n'
|
||||
append_part(f'<details type="tool_calls" done="false" id="{call_id}" name="{name}" arguments="{html.escape(json.dumps(arguments))}">\n<summary>Executing...</summary>\n</details>\n')
|
||||
|
||||
elif item_type == 'function_call_output':
|
||||
# Already handled inline with function_call above
|
||||
pass
|
||||
|
||||
elif item_type == 'reasoning':
|
||||
reasoning_content = ''
|
||||
reasoning_parts = []
|
||||
# Check for 'summary' (new structure) or 'content' (legacy/fallback)
|
||||
source_list = item.get('summary', []) or item.get('content', [])
|
||||
for content_part in source_list:
|
||||
if 'text' in content_part:
|
||||
reasoning_content += content_part.get('text', '')
|
||||
reasoning_parts.append(content_part.get('text', ''))
|
||||
elif 'summary' in content_part: # Handle potential nested logic if any
|
||||
pass
|
||||
|
||||
reasoning_content = reasoning_content.strip()
|
||||
reasoning_content = ''.join(reasoning_parts).strip()
|
||||
|
||||
duration = item.get('duration')
|
||||
status = item.get('status', 'in_progress')
|
||||
|
|
@ -471,8 +477,7 @@ def serialize_output(output: list) -> str:
|
|||
# render as done (a subsequent item means reasoning is complete)
|
||||
is_last_item = idx == len(output) - 1
|
||||
|
||||
if content and not content.endswith('\n'):
|
||||
content += '\n'
|
||||
ensure_trailing_newline(content_parts)
|
||||
|
||||
display = html.escape(
|
||||
'\n'.join(
|
||||
|
|
@ -481,19 +486,32 @@ def serialize_output(output: list) -> str:
|
|||
)
|
||||
|
||||
if status == 'completed' or duration is not None or not is_last_item:
|
||||
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'
|
||||
append_part(f'<details type="reasoning" done="true" duration="{duration or 0}">\n<summary>Thought for {duration or 0} seconds</summary>\n{display}\n</details>\n')
|
||||
else:
|
||||
content = f'{content}<details type="reasoning" done="false">\n<summary>Thinking…</summary>\n{display}\n</details>\n'
|
||||
append_part(f'<details type="reasoning" done="false">\n<summary>Thinking…</summary>\n{display}\n</details>\n')
|
||||
|
||||
elif item_type == 'open_webui:code_interpreter':
|
||||
content_stripped, original_whitespace = split_content_and_whitespace(content)
|
||||
if is_opening_code_block(content_stripped):
|
||||
content = content_stripped.rstrip('`').rstrip() + original_whitespace
|
||||
else:
|
||||
content = content_stripped + original_whitespace
|
||||
# Check if previous content ends with an opening code fence (e.g. ```python)
|
||||
# If so, strip the fence characters since code_interpreter replaces it
|
||||
# Odd fence_count means an unclosed (opening) fence
|
||||
if fence_count % 2 == 1:
|
||||
for i in range(len(content_parts) - 1, -1, -1):
|
||||
part = content_parts[i]
|
||||
stripped = part.rstrip()
|
||||
if stripped:
|
||||
trailing_ws = part[len(stripped):]
|
||||
new_stripped = re.sub(r'```[^\n]*$', '', stripped).rstrip()
|
||||
new_part = new_stripped + trailing_ws
|
||||
fence_count += new_part.count('```') - part.count('```')
|
||||
content_parts[i] = new_part
|
||||
break
|
||||
|
||||
if content and not content.endswith('\n'):
|
||||
content += '\n'
|
||||
# Remove trailing empty/whitespace-only parts left
|
||||
# after fence removal to avoid extra blank lines.
|
||||
while content_parts and not content_parts[-1].strip():
|
||||
fence_count -= content_parts.pop().count('```')
|
||||
|
||||
ensure_trailing_newline(content_parts)
|
||||
|
||||
# Render the code_interpreter item as a <details> block
|
||||
# so the frontend Collapsible renders "Analyzing..."/"Analyzed".
|
||||
|
|
@ -519,11 +537,11 @@ def serialize_output(output: list) -> str:
|
|||
output_attr = f' output="{html.escape(output_json)}"'
|
||||
|
||||
if status == 'completed' or duration is not None or not is_last_item:
|
||||
content += f'<details type="code_interpreter" done="true" duration="{duration or 0}"{output_attr}>\n<summary>Analyzed</summary>\n{display}\n</details>\n'
|
||||
append_part(f'<details type="code_interpreter" done="true" duration="{duration or 0}"{output_attr}>\n<summary>Analyzed</summary>\n{display}\n</details>\n')
|
||||
else:
|
||||
content += f'<details type="code_interpreter" done="false"{output_attr}>\n<summary>Analyzing…</summary>\n{display}\n</details>\n'
|
||||
append_part(f'<details type="code_interpreter" done="false"{output_attr}>\n<summary>Analyzing…</summary>\n{display}\n</details>\n')
|
||||
|
||||
return content.strip()
|
||||
return ''.join(content_parts).strip()
|
||||
|
||||
|
||||
def deep_merge(target, source):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue