Only print empty response warning log if there are tools

This commit is contained in:
Quinlan Jager 2025-05-05 23:06:59 -07:00
parent 097620026f
commit 5fd049b44b
2 changed files with 188 additions and 81 deletions

View file

@ -1571,13 +1571,13 @@ class Coder:
def _print_tool_call_info(self, server_tool_calls): def _print_tool_call_info(self, server_tool_calls):
"""Print information about an MCP tool call.""" """Print information about an MCP tool call."""
self.io.tool_output("Preparing to run MCP tools", bold=True)
for server, tool_calls in server_tool_calls.items(): for server, tool_calls in server_tool_calls.items():
for tool_call in tool_calls: for tool_call in tool_calls:
self.io.tool_output( self.io.tool_output(f"Tool Call: {tool_call.function.name}")
f"Running MCP tool: {tool_call.function.name} from server {server.name}" self.io.tool_output(f"Arguments: {tool_call.function.arguments}")
) self.io.tool_output(f"MCP Server: {server.name}")
self.io.tool_output(f"Tool arguments: {tool_call.function.arguments}")
if self.verbose: if self.verbose:
self.io.tool_output(f"Tool ID: {tool_call.id}") self.io.tool_output(f"Tool ID: {tool_call.id}")
@ -2058,7 +2058,7 @@ class Coder:
sys.stdout.flush() sys.stdout.flush()
yield text yield text
if not received_content: if not received_content and len(self.partial_response_tool_call) == 0:
self.io.tool_warning("Empty response received from LLM. Check your provider account?") self.io.tool_warning("Empty response received from LLM. Check your provider account?")
def live_incremental_response(self, final): def live_incremental_response(self, final):

View file

@ -1315,43 +1315,6 @@ This command will print 'Hello, World!' to the console."""
self.assertEqual(len(coder.mcp_tools), 1) self.assertEqual(len(coder.mcp_tools), 1)
self.assertEqual(coder.mcp_tools[0][0], "test_server") self.assertEqual(coder.mcp_tools[0][0], "test_server")
# Test execute_tool_calls
tool_call = MagicMock()
tool_call.function.name = "test_tool"
tool_call.function.arguments = "{}"
tool_call.id = "test_id"
tool_call.type = "function"
response = MagicMock()
response.choices = [MagicMock()]
response.choices[0].message.tool_calls = [tool_call]
# Setup mock for call_openai_tool
mock_call_result = MagicMock()
mock_call_result.content = [MagicMock()]
mock_call_result.content[0].text = "Tool execution result"
mock_mcp_client.call_openai_tool.return_value = mock_call_result
# Mock the async execution directly
with patch.object(
coder,
"execute_tool_calls",
return_value=[
{
"role": "tool",
"tool_call_id": "test_id",
"content": "Tool execution result",
}
],
):
tool_responses = coder.execute_tool_calls(response)
# Verify tool responses
self.assertEqual(len(tool_responses), 1)
self.assertEqual(tool_responses[0]["role"], "tool")
self.assertEqual(tool_responses[0]["tool_call_id"], "test_id")
self.assertEqual(tool_responses[0]["content"], "Tool execution result")
@patch("aider.coders.base_coder.experimental_mcp_client") @patch("aider.coders.base_coder.experimental_mcp_client")
def test_coder_creation_with_partial_failed_mcp_server(self, mock_mcp_client): def test_coder_creation_with_partial_failed_mcp_server(self, mock_mcp_client):
"""Test that a coder can still be created even if an MCP server fails to initialize.""" """Test that a coder can still be created even if an MCP server fails to initialize."""
@ -1449,55 +1412,199 @@ This command will print 'Hello, World!' to the console."""
"Error initializing MCP server failing_server:\nFailed to load tools" "Error initializing MCP server failing_server:\nFailed to load tools"
) )
@patch("aider.coders.base_coder.experimental_mcp_client") def test_process_tool_calls_none_response(self):
def test_initialize_mcp_tools(self, mock_mcp_client): """Test that process_tool_calls handles None response correctly."""
"""Test that the coder initializes MCP tools correctly."""
with GitTemporaryDirectory(): with GitTemporaryDirectory():
io = InputOutput(yes=True) io = InputOutput(yes=True)
coder = Coder.create(self.GPT35, "diff", io=io)
# Create mock MCP servers # Test with None response
mock_server1 = MagicMock() result = coder.process_tool_calls(None)
mock_server1.name = "server1" self.assertFalse(result)
mock_server1.connect = MagicMock()
mock_server1.disconnect = MagicMock()
mock_server2 = MagicMock() def test_process_tool_calls_no_tool_calls(self):
mock_server2.name = "server2" """Test that process_tool_calls handles response with no tool calls."""
mock_server2.connect = MagicMock() with GitTemporaryDirectory():
mock_server2.disconnect = MagicMock() io = InputOutput(yes=True)
coder = Coder.create(self.GPT35, "diff", io=io)
# Setup mock return values # Create a response with no tool calls
server1_tools = [{"function": {"name": "tool1", "description": "Tool 1 description"}}] response = MagicMock()
server2_tools = [{"function": {"name": "tool2", "description": "Tool 2 description"}}] response.choices = [MagicMock()]
response.choices[0].message = MagicMock()
response.choices[0].message.tool_calls = []
# Mock the initialize_mcp_tools method result = coder.process_tool_calls(response)
expected_tools = [("server1", server1_tools), ("server2", server2_tools)] self.assertFalse(result)
# Create coder with mock MCP servers and patch initialize_mcp_tools @patch("aider.coders.base_coder.experimental_mcp_client")
with patch.object(Coder, "initialize_mcp_tools"): @patch("asyncio.run")
coder = Coder.create( def test_process_tool_calls_with_tools(self, mock_asyncio_run, mock_mcp_client):
self.GPT35, """Test that process_tool_calls processes tool calls correctly."""
"diff", with GitTemporaryDirectory():
io=io, io = InputOutput(yes=True)
mcp_servers=[mock_server1, mock_server2], io.confirm_ask = MagicMock(return_value=True)
verbose=True,
)
# Manually set mcp_tools to expected value # Create mock MCP server
coder.mcp_tools = expected_tools mock_server = MagicMock()
mock_server.name = "test_server"
# Verify that mcp_tools contains the expected tools # Create a tool call
self.assertEqual(len(coder.mcp_tools), 2) tool_call = MagicMock()
self.assertEqual(coder.mcp_tools[0][0], "server1") tool_call.id = "test_id"
self.assertEqual(coder.mcp_tools[0][1], server1_tools) tool_call.type = "function"
self.assertEqual(coder.mcp_tools[1][0], "server2") tool_call.function = MagicMock()
self.assertEqual(coder.mcp_tools[1][1], server2_tools) tool_call.function.name = "test_tool"
tool_call.function.arguments = '{"param": "value"}'
# Test get_tool_list # Create a response with tool calls
tool_list = coder.get_tool_list() response = MagicMock()
self.assertEqual(len(tool_list), 2) response.choices = [MagicMock()]
self.assertEqual(tool_list[0], server1_tools[0]) response.choices[0].message = MagicMock()
self.assertEqual(tool_list[1], server2_tools[0]) response.choices[0].message.tool_calls = [tool_call]
response.choices[0].message.to_dict = MagicMock(
return_value={"role": "assistant", "tool_calls": [{"id": "test_id"}]}
)
# Create coder with mock MCP tools and servers
coder = Coder.create(self.GPT35, "diff", io=io)
coder.mcp_tools = [("test_server", [{"function": {"name": "test_tool"}}])]
coder.mcp_servers = [mock_server]
# Mock asyncio.run to return tool responses
tool_responses = [
[{"role": "tool", "tool_call_id": "test_id", "content": "Tool execution result"}]
]
mock_asyncio_run.return_value = tool_responses
# Test process_tool_calls
result = coder.process_tool_calls(response)
self.assertTrue(result)
# Verify that asyncio.run was called
mock_asyncio_run.assert_called_once()
# Verify that the messages were added
self.assertEqual(len(coder.cur_messages), 2)
self.assertEqual(coder.cur_messages[0]["role"], "assistant")
self.assertEqual(coder.cur_messages[1]["role"], "tool")
self.assertEqual(coder.cur_messages[1]["tool_call_id"], "test_id")
self.assertEqual(coder.cur_messages[1]["content"], "Tool execution result")
def test_process_tool_calls_max_calls_exceeded(self):
"""Test that process_tool_calls handles max tool calls exceeded."""
with GitTemporaryDirectory():
io = InputOutput(yes=True)
io.tool_warning = MagicMock()
# Create a tool call
tool_call = MagicMock()
tool_call.id = "test_id"
tool_call.type = "function"
tool_call.function = MagicMock()
tool_call.function.name = "test_tool"
# Create a response with tool calls
response = MagicMock()
response.choices = [MagicMock()]
response.choices[0].message = MagicMock()
response.choices[0].message.tool_calls = [tool_call]
# Create mock MCP server
mock_server = MagicMock()
mock_server.name = "test_server"
# Create coder with max tool calls exceeded
coder = Coder.create(self.GPT35, "diff", io=io)
coder.num_tool_calls = coder.max_tool_calls
coder.mcp_tools = [("test_server", [{"function": {"name": "test_tool"}}])]
coder.mcp_servers = [mock_server]
# Test process_tool_calls
result = coder.process_tool_calls(response)
self.assertFalse(result)
# Verify that warning was shown
io.tool_warning.assert_called_once_with(
f"Only {coder.max_tool_calls} tool calls allowed, stopping."
)
def test_process_tool_calls_user_rejects(self):
"""Test that process_tool_calls handles user rejection."""
with GitTemporaryDirectory():
io = InputOutput(yes=True)
io.confirm_ask = MagicMock(return_value=False)
# Create a tool call
tool_call = MagicMock()
tool_call.id = "test_id"
tool_call.type = "function"
tool_call.function = MagicMock()
tool_call.function.name = "test_tool"
# Create a response with tool calls
response = MagicMock()
response.choices = [MagicMock()]
response.choices[0].message = MagicMock()
response.choices[0].message.tool_calls = [tool_call]
# Create mock MCP server
mock_server = MagicMock()
mock_server.name = "test_server"
# Create coder with mock MCP tools
coder = Coder.create(self.GPT35, "diff", io=io)
coder.mcp_tools = [("test_server", [{"function": {"name": "test_tool"}}])]
coder.mcp_servers = [mock_server]
# Test process_tool_calls
result = coder.process_tool_calls(response)
self.assertFalse(result)
# Verify that confirm_ask was called
io.confirm_ask.assert_called_once_with("Run tools?")
# Verify that no messages were added
self.assertEqual(len(coder.cur_messages), 0)
@patch("asyncio.run")
def test_execute_tool_calls(self, mock_asyncio_run):
"""Test that _execute_tool_calls executes tool calls correctly."""
with GitTemporaryDirectory():
io = InputOutput(yes=True)
coder = Coder.create(self.GPT35, "diff", io=io)
# Create mock server and tool call
mock_server = MagicMock()
mock_server.name = "test_server"
tool_call = MagicMock()
tool_call.id = "test_id"
tool_call.type = "function"
tool_call.function = MagicMock()
tool_call.function.name = "test_tool"
tool_call.function.arguments = '{"param": "value"}'
# Create server_tool_calls
server_tool_calls = {mock_server: [tool_call]}
# Mock asyncio.run to return tool responses
tool_responses = [
[{"role": "tool", "tool_call_id": "test_id", "content": "Tool execution result"}]
]
mock_asyncio_run.return_value = tool_responses
# Test _execute_tool_calls directly
result = coder._execute_tool_calls(server_tool_calls)
# Verify that asyncio.run was called
mock_asyncio_run.assert_called_once()
# Verify that the correct tool responses were returned
self.assertEqual(len(result), 1)
self.assertEqual(result[0]["role"], "tool")
self.assertEqual(result[0]["tool_call_id"], "test_id")
self.assertEqual(result[0]["content"], "Tool execution result")
if __name__ == "__main__": if __name__ == "__main__":