This commit is contained in:
Timothy Jaeryang Baek
2026-08-23 02:34:08 -04:00
parent 4807866a1c
commit f3f76095d1
4 changed files with 183 additions and 40 deletions
+80 -39
View File
@@ -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(
{
+1 -1
View File
@@ -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')