diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index 67e79e270d7..126a4713fb5 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -141,6 +141,11 @@ async def _transform_stream( if isinstance(event.item, ResponseOutputMessage): yield {"role": event.item.role} 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 elif isinstance(event, ResponseOutputItemDoneEvent): item = event.item.model_dump() diff --git a/tests/components/openai_conversation/snapshots/test_conversation.ambr b/tests/components/openai_conversation/snapshots/test_conversation.ambr index 77c28de2773..0f874969aff 100644 --- a/tests/components/openai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/openai_conversation/snapshots/test_conversation.ambr @@ -17,13 +17,6 @@ }), 'tool_name': 'test_tool', }), - dict({ - 'id': 'call_call_2', - 'tool_args': dict({ - 'param1': 'call2', - }), - 'tool_name': 'test_tool', - }), ]), }), dict({ @@ -33,6 +26,20 @@ 'tool_name': 'test_tool', '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({ 'agent_id': 'conversation.openai', '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, + }), + ]) +# --- diff --git a/tests/components/openai_conversation/test_conversation.py b/tests/components/openai_conversation/test_conversation.py index 269590b483a..99559cb3b61 100644 --- a/tests/components/openai_conversation/test_conversation.py +++ b/tests/components/openai_conversation/test_conversation.py @@ -596,6 +596,48 @@ async def test_function_call( 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( ("description", "messages"), [