refac
This commit is contained in:
@@ -2,7 +2,6 @@ import ast
|
||||
import asyncio
|
||||
import base64
|
||||
import copy
|
||||
import html
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
@@ -378,210 +377,6 @@ 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 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
|
||||
|
||||
|
||||
_OPENAI_TOOL_DISPLAY_NAMES = {
|
||||
'web_search_call': 'Web Search',
|
||||
'file_search_call': 'File Search',
|
||||
'computer_call': 'Computer Use',
|
||||
}
|
||||
|
||||
|
||||
def _render_openai_tool_call_handler(item: dict, done: bool) -> str:
|
||||
"""Render an OpenAI Responses API server-side tool item as a <details> block.
|
||||
|
||||
Handles web_search_call, file_search_call, and computer_call items whose
|
||||
schemas are defined in the openai-python SDK (generated from OpenAPI spec).
|
||||
"""
|
||||
item_type = item.get('type', '')
|
||||
call_id = item.get('id', '')
|
||||
display_name = _OPENAI_TOOL_DISPLAY_NAMES.get(item_type, item_type)
|
||||
|
||||
# Build a short summary of what the tool did
|
||||
summary = ''
|
||||
if item_type == 'web_search_call':
|
||||
action = item.get('action', {})
|
||||
if isinstance(action, dict):
|
||||
atype = action.get('type', '')
|
||||
if atype == 'search':
|
||||
queries = action.get('queries') or []
|
||||
query = action.get('query', '')
|
||||
summary = (
|
||||
f'Search: {", ".join(str(q) for q in queries)}'
|
||||
if queries
|
||||
else (f'Search: {query}' if query else '')
|
||||
)
|
||||
elif atype == 'open_page':
|
||||
summary = f'Open page: {action.get("url", "")}' if action.get('url') else ''
|
||||
elif atype == 'find_in_page':
|
||||
summary = f'Find in page: {action.get("pattern", "")}' if action.get('pattern') else ''
|
||||
elif item_type == 'file_search_call':
|
||||
queries = item.get('queries', [])
|
||||
if queries:
|
||||
summary = f'Queries: {", ".join(str(q) for q in queries)}'
|
||||
elif item_type == 'computer_call':
|
||||
action = item.get('action')
|
||||
actions = item.get('actions')
|
||||
if isinstance(action, dict):
|
||||
summary = f'Action: {action.get("type", "unknown")}'
|
||||
elif isinstance(actions, list) and actions:
|
||||
summary = f'Actions: {", ".join(a.get("type", "?") for a in actions if isinstance(a, dict))}'
|
||||
|
||||
escaped_name = html.escape(display_name)
|
||||
if done:
|
||||
return f'<details type="tool_calls" done="true" id="{call_id}" name="{escaped_name}" arguments="">\n<summary>Tool Executed</summary>\n{html.escape(summary)}\n</details>\n'
|
||||
return f'<details type="tool_calls" done="false" id="{call_id}" name="{escaped_name}" arguments="">\n<summary>Executing...</summary>\n</details>\n'
|
||||
|
||||
|
||||
def serialize_output(output: list) -> str:
|
||||
"""
|
||||
Convert OR-aligned output items to HTML for display.
|
||||
For LLM consumption, use convert_output_to_messages() instead.
|
||||
"""
|
||||
parts: list[str] = []
|
||||
|
||||
# First pass: collect function_call_output items by call_id for lookup
|
||||
tool_outputs = {}
|
||||
for item in output:
|
||||
if item.get('type') == 'function_call_output':
|
||||
tool_outputs[item.get('call_id')] = item
|
||||
|
||||
# Second pass: render items in order
|
||||
for idx, item in enumerate(output):
|
||||
item_type = item.get('type', '')
|
||||
|
||||
if item_type == 'message':
|
||||
for content_part in item.get('content', []):
|
||||
if 'text' in content_part:
|
||||
text = content_part.get('text', '').strip()
|
||||
if text:
|
||||
parts.append(text)
|
||||
|
||||
elif item_type == 'function_call':
|
||||
call_id = item.get('call_id', '')
|
||||
name = item.get('name', '')
|
||||
arguments = item.get('arguments', '')
|
||||
|
||||
result_item = tool_outputs.get(call_id)
|
||||
if result_item:
|
||||
result_parts: list[str] = []
|
||||
for result_output in result_item.get('output', []):
|
||||
if 'text' in result_output:
|
||||
output_text = result_output.get('text', '')
|
||||
result_parts.append(str(output_text) if not isinstance(output_text, str) else output_text)
|
||||
result_text = ''.join(result_parts)
|
||||
files = result_item.get('files')
|
||||
embeds = result_item.get('embeds', '')
|
||||
|
||||
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>'
|
||||
)
|
||||
else:
|
||||
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>'
|
||||
)
|
||||
|
||||
elif item_type == 'function_call_output':
|
||||
# Already handled inline with function_call above
|
||||
pass
|
||||
|
||||
elif item_type in _OPENAI_TOOL_DISPLAY_NAMES:
|
||||
status = item.get('status', 'in_progress')
|
||||
done = status in ('completed', 'failed', 'incomplete') or idx != len(output) - 1
|
||||
parts.append(_render_openai_tool_call_handler(item, done).rstrip('\n'))
|
||||
|
||||
elif item_type == 'reasoning':
|
||||
reasoning_parts: list[str] = []
|
||||
# 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_parts.append(content_part.get('text', ''))
|
||||
elif 'summary' in content_part: # Handle potential nested logic if any
|
||||
pass
|
||||
|
||||
reasoning_content = ''.join(reasoning_parts).strip()
|
||||
|
||||
duration = item.get('duration')
|
||||
status = item.get('status', 'in_progress')
|
||||
|
||||
# Infer completion: if this reasoning item is NOT the last item,
|
||||
# render as done (a subsequent item means reasoning is complete)
|
||||
is_last_item = idx == len(output) - 1
|
||||
|
||||
display = html.escape(
|
||||
'\n'.join(
|
||||
(f'> {line}' if not line.startswith('>') else line) for line in reasoning_content.splitlines()
|
||||
)
|
||||
)
|
||||
|
||||
if status == 'completed' or duration is not None or not is_last_item:
|
||||
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>'
|
||||
)
|
||||
else:
|
||||
parts.append(
|
||||
f'<details type="reasoning" done="false">\n<summary>Thinking…</summary>\n{display}\n</details>'
|
||||
)
|
||||
|
||||
elif item_type == 'open_webui:code_interpreter':
|
||||
# Code interpreter needs to inspect/mutate prior accumulated content
|
||||
# to strip trailing unclosed code fences — materialize only here.
|
||||
content = '\n'.join(parts)
|
||||
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
|
||||
|
||||
# Re-split back into parts list after mutation
|
||||
parts = [content] if content else []
|
||||
|
||||
# Render the code_interpreter item as a <details> block
|
||||
# so the frontend Collapsible renders "Analyzing..."/"Analyzed".
|
||||
code = item.get('code', '').strip()
|
||||
lang = item.get('lang', 'python')
|
||||
status = item.get('status', 'in_progress')
|
||||
duration = item.get('duration')
|
||||
is_last_item = idx == len(output) - 1
|
||||
|
||||
# Build inner content: code block
|
||||
display = ''
|
||||
if code:
|
||||
display = f'```{lang}\n{code}\n```'
|
||||
|
||||
# Build output attribute as HTML-escaped JSON for CodeBlock.svelte
|
||||
ci_output = item.get('output')
|
||||
output_attr = ''
|
||||
if ci_output:
|
||||
if isinstance(ci_output, dict):
|
||||
output_json = json.dumps(ci_output, ensure_ascii=False)
|
||||
else:
|
||||
output_json = json.dumps({'result': str(ci_output)}, ensure_ascii=False)
|
||||
output_attr = f' output="{html.escape(output_json)}"'
|
||||
|
||||
if status == 'completed' or duration is not None or not is_last_item:
|
||||
parts.append(
|
||||
f'<details type="code_interpreter" done="true" duration="{duration or 0}"{output_attr}>\n<summary>Analyzed</summary>\n{display}\n</details>'
|
||||
)
|
||||
else:
|
||||
parts.append(
|
||||
f'<details type="code_interpreter" done="false"{output_attr}>\n<summary>Analyzing…</summary>\n{display}\n</details>'
|
||||
)
|
||||
|
||||
return '\n'.join(parts).strip()
|
||||
|
||||
|
||||
def deep_merge(target, source):
|
||||
"""
|
||||
Merge source into target recursively (returning new structure).
|
||||
@@ -3127,7 +2922,6 @@ def update_assistant_message_from_stream(assistant_message, raw):
|
||||
output, meta = handle_responses_streaming_event(data, assistant_message.get('output', []))
|
||||
if output:
|
||||
assistant_message['output'] = output
|
||||
assistant_message['content'] = serialize_output(output)
|
||||
if meta and meta.get('usage'):
|
||||
assistant_message['usage'] = merge_usage(assistant_message.get('usage'), meta['usage'])
|
||||
continue
|
||||
@@ -3527,18 +3321,16 @@ async def outlet_filter_handler(ctx):
|
||||
'output'
|
||||
)
|
||||
if content_changed or output_changed:
|
||||
# If output was modified, re-derive content from it
|
||||
new_content = message.get('content', original_message.get('content', ''))
|
||||
if output_changed:
|
||||
new_content = serialize_output(message['output'])
|
||||
message_update = {
|
||||
'originalContent': original_message.get('content'),
|
||||
**({'output': message['output']} if output_changed else {}),
|
||||
}
|
||||
if content_changed:
|
||||
message_update['content'] = message.get('content', '')
|
||||
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||
chat_id,
|
||||
outlet_message_id,
|
||||
{
|
||||
'content': new_content,
|
||||
'originalContent': original_message.get('content'),
|
||||
**({'output': message['output']} if output_changed else {}),
|
||||
},
|
||||
message_update,
|
||||
)
|
||||
|
||||
if event_emitter:
|
||||
@@ -3722,7 +3514,7 @@ async def non_streaming_chat_response_handler(response, ctx):
|
||||
if ENABLE_API_OUTLET_FILTERS and (content or output):
|
||||
usage = normalize_usage(response_data.get('usage', {}) or {})
|
||||
ctx['assistant_message'] = {
|
||||
'content': content or serialize_output(output),
|
||||
**({'content': content} if content else {}),
|
||||
**({'output': output} if output else {}),
|
||||
**({'usage': usage} if usage else {}),
|
||||
}
|
||||
@@ -4242,7 +4034,6 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
|
||||
processed_data = {
|
||||
'output': full_output(),
|
||||
'content': serialize_output(full_output()),
|
||||
}
|
||||
|
||||
# print(data)
|
||||
@@ -4412,7 +4203,6 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
)
|
||||
|
||||
data = {
|
||||
'content': serialize_output(full_output() + pending_fc_items),
|
||||
'output': full_output() + pending_fc_items,
|
||||
}
|
||||
delta_type = 'tool_call'
|
||||
@@ -4474,7 +4264,6 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
]
|
||||
|
||||
data = {
|
||||
'content': serialize_output(full_output()),
|
||||
'output': full_output(),
|
||||
}
|
||||
delta_type = 'content'
|
||||
@@ -4637,18 +4426,15 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
metadata['chat_id'],
|
||||
metadata['message_id'],
|
||||
{
|
||||
'content': serialize_output(full_output()),
|
||||
'output': full_output(),
|
||||
},
|
||||
)
|
||||
data = {
|
||||
'content': serialize_output(full_output()),
|
||||
'output': full_output(),
|
||||
}
|
||||
delta_type = 'content'
|
||||
else:
|
||||
data = {
|
||||
'content': serialize_output(full_output()),
|
||||
'output': full_output(),
|
||||
}
|
||||
delta_type = 'content'
|
||||
@@ -4791,7 +4577,6 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
{
|
||||
'type': 'chat:completion',
|
||||
'data': {
|
||||
'content': serialize_output(full_output()),
|
||||
'output': full_output(),
|
||||
},
|
||||
}
|
||||
@@ -4948,7 +4733,7 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
display_files = []
|
||||
for file_item in result.get('files', []):
|
||||
if file_item.get('type') == 'image' and file_item.get('url', '').startswith('data:'):
|
||||
# LLM-only: add as input_image part (invisible to serialize_output)
|
||||
# LLM-only: add as input_image part, not frontend display output.
|
||||
output_parts.append({'type': 'input_image', 'image_url': file_item['url']})
|
||||
else:
|
||||
# Frontend display (MCP images, audio, etc.)
|
||||
@@ -5054,7 +4839,6 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
{
|
||||
'type': 'chat:completion',
|
||||
'data': {
|
||||
'content': serialize_output(output),
|
||||
'output': frontend_output,
|
||||
},
|
||||
}
|
||||
@@ -5178,7 +4962,6 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
{
|
||||
'type': 'chat:completion',
|
||||
'data': {
|
||||
'content': serialize_output(output),
|
||||
'output': output,
|
||||
},
|
||||
}
|
||||
@@ -5305,7 +5088,6 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
{
|
||||
'type': 'chat:completion',
|
||||
'data': {
|
||||
'content': serialize_output(output),
|
||||
'output': output,
|
||||
},
|
||||
}
|
||||
@@ -5352,7 +5134,6 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
)
|
||||
data = {
|
||||
'done': True,
|
||||
'content': serialize_output(output),
|
||||
'output': output,
|
||||
'title': title,
|
||||
**({'usage': usage} if usage else {}),
|
||||
@@ -5366,7 +5147,6 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
metadata['message_id'],
|
||||
{
|
||||
'done': True,
|
||||
'content': serialize_output(output),
|
||||
'output': output,
|
||||
**({'usage': usage} if usage else {}),
|
||||
},
|
||||
@@ -5409,7 +5189,6 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
)
|
||||
|
||||
ctx['assistant_message'] = {
|
||||
'content': serialize_output(output),
|
||||
'output': output,
|
||||
**({'usage': usage} if usage else {}),
|
||||
}
|
||||
@@ -5437,7 +5216,6 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
metadata['message_id'],
|
||||
{
|
||||
'done': True,
|
||||
'content': serialize_output(output),
|
||||
'output': output,
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user