mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-26 23:44:48 -06:00
refac
This commit is contained in:
@@ -142,6 +142,48 @@ logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _is_tool_result_error(value: Any) -> bool:
|
||||
if isinstance(value, str):
|
||||
text = value.strip().lower()
|
||||
if (
|
||||
text.startswith('error:')
|
||||
or text.startswith('exception:')
|
||||
or text.startswith('traceback')
|
||||
or text.startswith('http error!')
|
||||
):
|
||||
return True
|
||||
|
||||
parsed = value
|
||||
while isinstance(parsed, str):
|
||||
try:
|
||||
parsed = JSONCodec.loads(parsed)
|
||||
except (JSONCodec.JSONDecodeError, TypeError, ValueError):
|
||||
break
|
||||
|
||||
if not isinstance(parsed, dict):
|
||||
return False
|
||||
|
||||
error = parsed.get('error')
|
||||
if isinstance(error, str):
|
||||
has_error = bool(error.strip())
|
||||
else:
|
||||
has_error = isinstance(error, (dict, list)) and bool(error)
|
||||
if has_error:
|
||||
return True
|
||||
|
||||
status = parsed.get('status')
|
||||
if isinstance(status, str) and status.strip().lower() in {'error', 'failed'}:
|
||||
return True
|
||||
|
||||
if parsed.get('success') is False or parsed.get('ok') is False:
|
||||
message = parsed.get('message')
|
||||
return has_error or (
|
||||
bool(message.strip()) if isinstance(message, str) else isinstance(message, (dict, list)) and bool(message)
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
async def publish_chat_finished_event(
|
||||
request: Request, user: UserModel, metadata: dict, title: str, content: str, output: list | None = None
|
||||
):
|
||||
@@ -1299,7 +1341,7 @@ async def chat_completion_tools_handler(
|
||||
tool_result = await tool_function(**tool_function_params)
|
||||
|
||||
except Exception as e:
|
||||
tool_result = str(e)
|
||||
tool_result = {'error': str(e)}
|
||||
|
||||
tool_result, tool_result_files, tool_result_embeds = await process_tool_result(
|
||||
request,
|
||||
@@ -3076,7 +3118,7 @@ async def execute_tool_call_for_output(request, form_data, user, metadata, event
|
||||
)
|
||||
result = await function(**params)
|
||||
except Exception as e:
|
||||
result = str(e)
|
||||
result = {'error': str(e)}
|
||||
|
||||
result, files, embeds = await process_tool_result(
|
||||
request,
|
||||
@@ -3098,28 +3140,6 @@ async def execute_tool_call_for_output(request, form_data, user, metadata, event
|
||||
}
|
||||
|
||||
|
||||
def append_tool_result_output(output: list[dict], result: dict) -> None:
|
||||
output_parts = [{'type': 'input_text', 'text': result.get('content', '')}]
|
||||
display_files = []
|
||||
for file_item in result.get('files', []):
|
||||
if file_item.get('type') == 'image' and file_item.get('url', '').startswith('data:'):
|
||||
output_parts.append({'type': 'input_image', 'image_url': file_item['url']})
|
||||
else:
|
||||
display_files.append(file_item)
|
||||
|
||||
output.append(
|
||||
{
|
||||
'type': 'function_call_output',
|
||||
'id': output_id('fco'),
|
||||
'call_id': result.get('tool_call_id', ''),
|
||||
'output': output_parts,
|
||||
'status': 'completed',
|
||||
**({'files': display_files} if display_files else {}),
|
||||
**({'embeds': result.get('embeds')} if result.get('embeds') else {}),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def drain_approved_tool_calls(request, form_data, user, model, metadata) -> bool:
|
||||
chat_id = metadata.get('chat_id')
|
||||
message_id = metadata.get('message_id') or metadata.get('assistant_message_id')
|
||||
@@ -3186,9 +3206,27 @@ async def drain_approved_tool_calls(request, form_data, user, model, metadata) -
|
||||
event_emitter,
|
||||
tool_call,
|
||||
)
|
||||
item['status'] = 'completed'
|
||||
item['arguments'] = tool_call.get('function', {}).get('arguments', '{}')
|
||||
append_tool_result_output(output, result)
|
||||
output_parts = [{'type': 'input_text', 'text': result.get('content', '')}]
|
||||
item['status'] = 'failed' if _is_tool_result_error(result.get('content', '')) else 'completed'
|
||||
display_files = []
|
||||
for file_item in result.get('files', []):
|
||||
if file_item.get('type') == 'image' and file_item.get('url', '').startswith('data:'):
|
||||
output_parts.append({'type': 'input_image', 'image_url': file_item['url']})
|
||||
else:
|
||||
display_files.append(file_item)
|
||||
|
||||
output.append(
|
||||
{
|
||||
'type': 'function_call_output',
|
||||
'id': output_id('fco'),
|
||||
'call_id': result.get('tool_call_id', ''),
|
||||
'output': output_parts,
|
||||
'status': item['status'],
|
||||
**({'files': display_files} if display_files else {}),
|
||||
**({'embeds': result.get('embeds')} if result.get('embeds') else {}),
|
||||
}
|
||||
)
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
@@ -5489,7 +5527,7 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
)
|
||||
result = await function(**params)
|
||||
except Exception as e:
|
||||
result = str(e)
|
||||
result = {'error': str(e)}
|
||||
return params, result, tool, tool_type, direct_tool
|
||||
|
||||
delegate_calls = [
|
||||
@@ -5575,19 +5613,13 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
}
|
||||
)
|
||||
|
||||
# Update function_call statuses and append function_call_output items
|
||||
for tc in response_tool_calls:
|
||||
call_id = tc.get('id', '')
|
||||
# Mark function_call as completed
|
||||
for item in output:
|
||||
if item.get('type') == 'function_call' and item.get('call_id') == call_id:
|
||||
item['status'] = 'completed'
|
||||
# Update arguments with parsed/sanitized version
|
||||
item['arguments'] = tc.get('function', {}).get('arguments', '{}')
|
||||
break
|
||||
|
||||
result_status_by_call_id = {}
|
||||
for result in results:
|
||||
output_parts = [{'type': 'input_text', 'text': result.get('content', '')}]
|
||||
local_output_status = (
|
||||
'failed' if _is_tool_result_error(result.get('content', '')) else 'completed'
|
||||
)
|
||||
result_status_by_call_id[result.get('tool_call_id', '')] = local_output_status
|
||||
|
||||
# Separate image data URIs (for LLM via input_image) from
|
||||
# other files (for frontend display via files attribute).
|
||||
@@ -5606,12 +5638,21 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
'id': output_id('fco'),
|
||||
'call_id': result.get('tool_call_id', ''),
|
||||
'output': output_parts,
|
||||
'status': 'completed',
|
||||
'status': local_output_status,
|
||||
**({'files': display_files} if display_files else {}),
|
||||
**({'embeds': result.get('embeds')} if result.get('embeds') else {}),
|
||||
}
|
||||
)
|
||||
|
||||
# Update function_call statuses and parsed/sanitized arguments.
|
||||
for tc in response_tool_calls:
|
||||
call_id = tc.get('id', '')
|
||||
for item in output:
|
||||
if item.get('type') == 'function_call' and item.get('call_id') == call_id:
|
||||
item['status'] = result_status_by_call_id.get(call_id, 'completed')
|
||||
item['arguments'] = tc.get('function', {}).get('arguments', '{}')
|
||||
break
|
||||
|
||||
# Append a new empty message item for the next response
|
||||
output.append(
|
||||
{
|
||||
|
||||
@@ -312,7 +312,7 @@ def convert_output_to_messages(
|
||||
for item in output
|
||||
if item.get('type') == 'function_call'
|
||||
and item.get('call_id')
|
||||
and item.get('status') in {'completed', 'rejected'}
|
||||
and item.get('status') in {'completed', 'failed', 'rejected'}
|
||||
}
|
||||
result_call_ids = {
|
||||
item.get('call_id') for item in output if item.get('type') == 'function_call_output' and item.get('call_id')
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
export let id = '';
|
||||
export let tokens: Array<{
|
||||
summary?: string;
|
||||
text?: string;
|
||||
attributes?: {
|
||||
type?: string;
|
||||
name?: string;
|
||||
@@ -50,6 +51,49 @@
|
||||
}
|
||||
}
|
||||
|
||||
function isToolResultError(value: unknown): boolean {
|
||||
if (typeof value === 'string') {
|
||||
const text = value.trim().toLowerCase();
|
||||
if (
|
||||
text.startsWith('error:') ||
|
||||
text.startsWith('exception:') ||
|
||||
text.startsWith('traceback') ||
|
||||
text.startsWith('http error!')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
let parsed = value;
|
||||
while (typeof parsed === 'string') {
|
||||
try {
|
||||
parsed = JSON.parse(parsed);
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return false;
|
||||
|
||||
const result = parsed as Record<string, unknown>;
|
||||
const error = result.error;
|
||||
if (
|
||||
(typeof error === 'string' && error.trim().length > 0) ||
|
||||
(typeof error === 'object' && error !== null)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const status = typeof result.status === 'string' ? result.status.trim().toLowerCase() : '';
|
||||
if (status === 'error' || status === 'failed') return true;
|
||||
|
||||
const message = result.message;
|
||||
return (
|
||||
(result.success === false || result.ok === false) &&
|
||||
((typeof message === 'string' && message.trim().length > 0) ||
|
||||
(typeof message === 'object' && message !== null))
|
||||
);
|
||||
}
|
||||
|
||||
$: toolCallCount = tokens.filter((t) => t?.attributes?.type === 'tool_calls').length;
|
||||
$: reasoningCount = tokens.filter((t) => t?.attributes?.type === 'reasoning').length;
|
||||
$: pendingToolTokens = tokens.filter(
|
||||
@@ -66,6 +110,12 @@
|
||||
$: hasRejected = tokens.some(
|
||||
(t) => t?.attributes?.type === 'tool_calls' && t?.attributes?.status === 'rejected'
|
||||
);
|
||||
$: hasError = tokens.some(
|
||||
(t) =>
|
||||
t?.attributes?.type === 'tool_calls' &&
|
||||
(t?.attributes?.status === 'failed' ||
|
||||
(t?.attributes?.done === 'true' && isToolResultError(decode(t?.text ?? ''))))
|
||||
);
|
||||
|
||||
$: codeInterpreterCount = tokens.filter((t) => t?.attributes?.type === 'code_interpreter').length;
|
||||
|
||||
@@ -157,6 +207,10 @@
|
||||
<div class="text-red-400 dark:text-red-500">
|
||||
<XMark className="size-4" strokeWidth="2.5" />
|
||||
</div>
|
||||
{:else if toolCallCount > 0 && hasError}
|
||||
<div class="text-red-500 dark:text-red-400">
|
||||
<XMark className="size-4" strokeWidth="2.5" />
|
||||
</div>
|
||||
{:else if toolCallCount > 0}
|
||||
<div class="text-emerald-500 dark:text-emerald-400">
|
||||
<CheckCircle className="size-4" strokeWidth="2" />
|
||||
|
||||
@@ -90,6 +90,49 @@
|
||||
}
|
||||
}
|
||||
|
||||
function isToolResultError(value: unknown): boolean {
|
||||
if (typeof value === 'string') {
|
||||
const text = value.trim().toLowerCase();
|
||||
if (
|
||||
text.startsWith('error:') ||
|
||||
text.startsWith('exception:') ||
|
||||
text.startsWith('traceback') ||
|
||||
text.startsWith('http error!')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
let parsed = value;
|
||||
while (typeof parsed === 'string') {
|
||||
try {
|
||||
parsed = JSON.parse(parsed);
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return false;
|
||||
|
||||
const result = parsed as Record<string, unknown>;
|
||||
const error = result.error;
|
||||
if (
|
||||
(typeof error === 'string' && error.trim().length > 0) ||
|
||||
(typeof error === 'object' && error !== null)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const status = typeof result.status === 'string' ? result.status.trim().toLowerCase() : '';
|
||||
if (status === 'error' || status === 'failed') return true;
|
||||
|
||||
const message = result.message;
|
||||
return (
|
||||
(result.success === false || result.ok === false) &&
|
||||
((typeof message === 'string' && message.trim().length > 0) ||
|
||||
(typeof message === 'object' && message !== null))
|
||||
);
|
||||
}
|
||||
|
||||
export let resultContent: string = '';
|
||||
|
||||
$: result = resultContent || decode(attributes?.result ?? '');
|
||||
@@ -110,6 +153,7 @@
|
||||
$: isExecuting = !isDone && !isRejected && attributes?.status === 'completed';
|
||||
$: isPreparing = !isDone && !isRejected && !needsApproval && !needsInput && !isExecuting;
|
||||
$: isActive = isPreparing || isExecuting;
|
||||
$: isError = attributes?.status === 'failed' || (isDone && isToolResultError(result));
|
||||
|
||||
$: parsedArgs = parseArguments(args);
|
||||
$: parsedResult = parseJSONString(result);
|
||||
@@ -171,6 +215,10 @@
|
||||
<div class="text-red-400 dark:text-red-500">
|
||||
<XMark className="size-4" strokeWidth="2.5" />
|
||||
</div>
|
||||
{:else if isError}
|
||||
<div class="text-red-500 dark:text-red-400">
|
||||
<XMark className="size-4" strokeWidth="2.5" />
|
||||
</div>
|
||||
{:else if isDone}
|
||||
<div class="text-emerald-500 dark:text-emerald-400">
|
||||
<CheckCircle className="size-4" strokeWidth="2" />
|
||||
|
||||
Reference in New Issue
Block a user