Ensure that OpenAI tool call deltas have a role (#145085)

This commit is contained in:
Paulus Schoutsen 2025-05-17 12:36:14 -04:00 committed by GitHub
parent 180e1f462c
commit 2956f4fea1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 96 additions and 7 deletions

View File

@ -141,6 +141,11 @@ async def _transform_stream(
if isinstance(event.item, ResponseOutputMessage): if isinstance(event.item, ResponseOutputMessage):
yield {"role": event.item.role} yield {"role": event.item.role}
elif isinstance(event.item, ResponseFunctionToolCall): elif isinstance(event.item, ResponseFunctionToolCall):
# OpenAI has tool calls as individual events
# while HA puts tool calls inside the assistant message.
# We turn them into individual assistant content for HA
# to ensure that tools are called as soon as possible.
yield {"role": "assistant"}
current_tool_call = event.item current_tool_call = event.item
elif isinstance(event, ResponseOutputItemDoneEvent): elif isinstance(event, ResponseOutputItemDoneEvent):
item = event.item.model_dump() item = event.item.model_dump()

View File

@ -17,13 +17,6 @@
}), }),
'tool_name': 'test_tool', 'tool_name': 'test_tool',
}), }),
dict({
'id': 'call_call_2',
'tool_args': dict({
'param1': 'call2',
}),
'tool_name': 'test_tool',
}),
]), ]),
}), }),
dict({ dict({
@ -33,6 +26,20 @@
'tool_name': 'test_tool', 'tool_name': 'test_tool',
'tool_result': 'value1', 'tool_result': 'value1',
}), }),
dict({
'agent_id': 'conversation.openai',
'content': None,
'role': 'assistant',
'tool_calls': list([
dict({
'id': 'call_call_2',
'tool_args': dict({
'param1': 'call2',
}),
'tool_name': 'test_tool',
}),
]),
}),
dict({ dict({
'agent_id': 'conversation.openai', 'agent_id': 'conversation.openai',
'role': 'tool_result', 'role': 'tool_result',
@ -48,3 +55,38 @@
}), }),
]) ])
# --- # ---
# name: test_function_call_without_reasoning
list([
dict({
'content': 'Please call the test function',
'role': 'user',
}),
dict({
'agent_id': 'conversation.openai',
'content': None,
'role': 'assistant',
'tool_calls': list([
dict({
'id': 'call_call_1',
'tool_args': dict({
'param1': 'call1',
}),
'tool_name': 'test_tool',
}),
]),
}),
dict({
'agent_id': 'conversation.openai',
'role': 'tool_result',
'tool_call_id': 'call_call_1',
'tool_name': 'test_tool',
'tool_result': 'value1',
}),
dict({
'agent_id': 'conversation.openai',
'content': 'Cool',
'role': 'assistant',
'tool_calls': None,
}),
])
# ---

View File

@ -596,6 +596,48 @@ async def test_function_call(
assert mock_chat_log.content[1:] == snapshot assert mock_chat_log.content[1:] == snapshot
async def test_function_call_without_reasoning(
hass: HomeAssistant,
mock_config_entry_with_assist: MockConfigEntry,
mock_init_component,
mock_create_stream: AsyncMock,
mock_chat_log: MockChatLog, # noqa: F811
snapshot: SnapshotAssertion,
) -> None:
"""Test function call from the assistant."""
mock_create_stream.return_value = [
# Initial conversation
(
*create_function_tool_call_item(
id="fc_1",
arguments=['{"para', 'm1":"call1"}'],
call_id="call_call_1",
name="test_tool",
output_index=1,
),
),
# Response after tool responses
create_message_item(id="msg_A", text="Cool", output_index=0),
]
mock_chat_log.mock_tool_results(
{
"call_call_1": "value1",
}
)
result = await conversation.async_converse(
hass,
"Please call the test function",
mock_chat_log.conversation_id,
Context(),
agent_id="conversation.openai",
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
# Don't test the prompt, as it's not deterministic
assert mock_chat_log.content[1:] == snapshot
@pytest.mark.parametrize( @pytest.mark.parametrize(
("description", "messages"), ("description", "messages"),
[ [