@@ -455,7 +455,7 @@ def serialize_output(output: list) -> str:
455455 Convert OR-aligned output items to HTML for display.
456456 For LLM consumption, use convert_output_to_messages() instead.
457457 """
458- content = ''
458+ parts : list [ str ] = []
459459
460460 # First pass: collect function_call_output items by call_id for lookup
461461 tool_outputs = {}
@@ -472,53 +472,48 @@ def serialize_output(output: list) -> str:
472472 if 'text' in content_part :
473473 text = content_part .get ('text' , '' ).strip ()
474474 if text :
475- content = f' { content } { text } \n '
475+ parts . append ( text )
476476
477477 elif item_type == 'function_call' :
478- # Render tool call inline with its result (if available)
479- if content and not content .endswith ('\n ' ):
480- content += '\n '
481-
482478 call_id = item .get ('call_id' , '' )
483479 name = item .get ('name' , '' )
484480 arguments = item .get ('arguments' , '' )
485481
486482 result_item = tool_outputs .get (call_id )
487483 if result_item :
488- result_text = ''
484+ result_parts : list [ str ] = []
489485 for result_output in result_item .get ('output' , []):
490486 if 'text' in result_output :
491487 output_text = result_output .get ('text' , '' )
492- result_text += str (output_text ) if not isinstance (output_text , str ) else output_text
488+ result_parts .append (str (output_text ) if not isinstance (output_text , str ) else output_text )
489+ result_text = '' .join (result_parts )
493490 files = result_item .get ('files' )
494491 embeds = result_item .get ('embeds' , '' )
495492
496- content += f'<details type="tool_calls" done="true" id="{ call_id } " name="{ name } " arguments="{ html .escape (json .dumps (arguments ))} " files="{ html .escape (json .dumps (files )) if files else "" } " embeds="{ html .escape (json .dumps (embeds ))} ">\n <summary>Tool Executed</summary>\n { html .escape (json .dumps (result_text , ensure_ascii = False ))} \n </details>\n '
493+ parts . append ( f'<details type="tool_calls" done="true" id="{ call_id } " name="{ name } " arguments="{ html .escape (json .dumps (arguments ))} " files="{ html .escape (json .dumps (files )) if files else "" } " embeds="{ html .escape (json .dumps (embeds ))} ">\n <summary>Tool Executed</summary>\n { html .escape (json .dumps (result_text , ensure_ascii = False ))} \n </details>' )
497494 else :
498- 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 '
495+ parts . append ( f'<details type="tool_calls" done="false" id="{ call_id } " name="{ name } " arguments="{ html .escape (json .dumps (arguments ))} ">\n <summary>Executing...</summary>\n </details>' )
499496
500497 elif item_type == 'function_call_output' :
501498 # Already handled inline with function_call above
502499 pass
503500
504501 elif item_type in _OPENAI_TOOL_DISPLAY_NAMES :
505- if content and not content .endswith ('\n ' ):
506- content += '\n '
507502 status = item .get ('status' , 'in_progress' )
508503 done = status in ('completed' , 'failed' , 'incomplete' ) or idx != len (output ) - 1
509- content += _render_openai_tool_call_handler (item , done )
504+ parts . append ( _render_openai_tool_call_handler (item , done ). rstrip ( ' \n ' ) )
510505
511506 elif item_type == 'reasoning' :
512- reasoning_content = ''
507+ reasoning_parts : list [ str ] = []
513508 # Check for 'summary' (new structure) or 'content' (legacy/fallback)
514509 source_list = item .get ('summary' , []) or item .get ('content' , [])
515510 for content_part in source_list :
516511 if 'text' in content_part :
517- reasoning_content += content_part .get ('text' , '' )
512+ reasoning_parts . append ( content_part .get ('text' , '' ) )
518513 elif 'summary' in content_part : # Handle potential nested logic if any
519514 pass
520515
521- reasoning_content = reasoning_content .strip ()
516+ reasoning_content = '' . join ( reasoning_parts ) .strip ()
522517
523518 duration = item .get ('duration' )
524519 status = item .get ('status' , 'in_progress' )
@@ -527,29 +522,29 @@ def serialize_output(output: list) -> str:
527522 # render as done (a subsequent item means reasoning is complete)
528523 is_last_item = idx == len (output ) - 1
529524
530- if content and not content .endswith ('\n ' ):
531- content += '\n '
532-
533525 display = html .escape (
534526 '\n ' .join (
535527 (f'> { line } ' if not line .startswith ('>' ) else line ) for line in reasoning_content .splitlines ()
536528 )
537529 )
538530
539531 if status == 'completed' or duration is not None or not is_last_item :
540- 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 '
532+ parts . append ( f' <details type="reasoning" done="true" duration="{ duration or 0 } ">\n <summary>Thought for { duration or 0 } seconds</summary>\n { display } \n </details>' )
541533 else :
542- content = f' { content } <details type="reasoning" done="false">\n <summary>Thinking…</summary>\n { display } \n </details>\n '
534+ parts . append ( f' <details type="reasoning" done="false">\n <summary>Thinking…</summary>\n { display } \n </details>' )
543535
544536 elif item_type == 'open_webui:code_interpreter' :
537+ # Code interpreter needs to inspect/mutate prior accumulated content
538+ # to strip trailing unclosed code fences — materialize only here.
539+ content = '\n ' .join (parts )
545540 content_stripped , original_whitespace = split_content_and_whitespace (content )
546541 if is_opening_code_block (content_stripped ):
547542 content = content_stripped .rstrip ('`' ).rstrip () + original_whitespace
548543 else :
549544 content = content_stripped + original_whitespace
550545
551- if content and not content . endswith ( ' \n ' ):
552- content += ' \n '
546+ # Re-split back into parts list after mutation
547+ parts = [ content ] if content else []
553548
554549 # Render the code_interpreter item as a <details> block
555550 # so the frontend Collapsible renders "Analyzing..."/"Analyzed".
@@ -575,11 +570,11 @@ def serialize_output(output: list) -> str:
575570 output_attr = f' output="{ html .escape (output_json )} "'
576571
577572 if status == 'completed' or duration is not None or not is_last_item :
578- content += f'<details type="code_interpreter" done="true" duration="{ duration or 0 } "{ output_attr } >\n <summary>Analyzed</summary>\n { display } \n </details>\n '
573+ parts . append ( f'<details type="code_interpreter" done="true" duration="{ duration or 0 } "{ output_attr } >\n <summary>Analyzed</summary>\n { display } \n </details>' )
579574 else :
580- content += f'<details type="code_interpreter" done="false"{ output_attr } >\n <summary>Analyzing…</summary>\n { display } \n </details>\n '
575+ parts . append ( f'<details type="code_interpreter" done="false"{ output_attr } >\n <summary>Analyzing…</summary>\n { display } \n </details>' )
581576
582- return content .strip ()
577+ return ' \n ' . join ( parts ) .strip ()
583578
584579
585580def deep_merge (target , source ):
0 commit comments