Spaces:
Build error
Build error
| """Test function calling module.""" | |
| import json | |
| from unittest.mock import patch | |
| import pytest | |
| from litellm import ModelResponse | |
| from openhands.agenthub.codeact_agent.function_calling import response_to_actions | |
| from openhands.core.exceptions import FunctionCallValidationError | |
| from openhands.events.action import ( | |
| BrowseInteractiveAction, | |
| CmdRunAction, | |
| FileEditAction, | |
| FileReadAction, | |
| IPythonRunCellAction, | |
| ) | |
| from openhands.events.event import FileEditSource, FileReadSource | |
| def create_mock_response(function_name: str, arguments: dict) -> ModelResponse: | |
| """Helper function to create a mock response with a tool call.""" | |
| return ModelResponse( | |
| id='mock-id', | |
| choices=[ | |
| { | |
| 'message': { | |
| 'tool_calls': [ | |
| { | |
| 'function': { | |
| 'name': function_name, | |
| 'arguments': json.dumps(arguments), | |
| }, | |
| 'id': 'mock-tool-call-id', | |
| 'type': 'function', | |
| } | |
| ], | |
| 'content': None, | |
| 'role': 'assistant', | |
| }, | |
| 'index': 0, | |
| 'finish_reason': 'tool_calls', | |
| } | |
| ], | |
| ) | |
| def test_execute_bash_valid(): | |
| """Test execute_bash with valid arguments.""" | |
| response = create_mock_response( | |
| 'execute_bash', {'command': 'ls', 'is_input': 'false'} | |
| ) | |
| actions = response_to_actions(response) | |
| assert len(actions) == 1 | |
| assert isinstance(actions[0], CmdRunAction) | |
| assert actions[0].command == 'ls' | |
| assert actions[0].is_input is False | |
| # Test with timeout parameter | |
| with patch.object(CmdRunAction, 'set_hard_timeout') as mock_set_hard_timeout: | |
| response_with_timeout = create_mock_response( | |
| 'execute_bash', {'command': 'ls', 'is_input': 'false', 'timeout': 30} | |
| ) | |
| actions_with_timeout = response_to_actions(response_with_timeout) | |
| # Verify set_hard_timeout was called with the correct value | |
| mock_set_hard_timeout.assert_called_once_with(30.0) | |
| assert len(actions_with_timeout) == 1 | |
| assert isinstance(actions_with_timeout[0], CmdRunAction) | |
| assert actions_with_timeout[0].command == 'ls' | |
| assert actions_with_timeout[0].is_input is False | |
| def test_execute_bash_missing_command(): | |
| """Test execute_bash with missing command argument.""" | |
| response = create_mock_response('execute_bash', {'is_input': 'false'}) | |
| with pytest.raises(FunctionCallValidationError) as exc_info: | |
| response_to_actions(response) | |
| assert 'Missing required argument "command"' in str(exc_info.value) | |
| def test_execute_ipython_cell_valid(): | |
| """Test execute_ipython_cell with valid arguments.""" | |
| response = create_mock_response('execute_ipython_cell', {'code': "print('hello')"}) | |
| actions = response_to_actions(response) | |
| assert len(actions) == 1 | |
| assert isinstance(actions[0], IPythonRunCellAction) | |
| assert actions[0].code == "print('hello')" | |
| def test_execute_ipython_cell_missing_code(): | |
| """Test execute_ipython_cell with missing code argument.""" | |
| response = create_mock_response('execute_ipython_cell', {}) | |
| with pytest.raises(FunctionCallValidationError) as exc_info: | |
| response_to_actions(response) | |
| assert 'Missing required argument "code"' in str(exc_info.value) | |
| def test_edit_file_valid(): | |
| """Test edit_file with valid arguments.""" | |
| response = create_mock_response( | |
| 'edit_file', | |
| {'path': '/path/to/file', 'content': 'file content', 'start': 1, 'end': 10}, | |
| ) | |
| actions = response_to_actions(response) | |
| assert len(actions) == 1 | |
| assert isinstance(actions[0], FileEditAction) | |
| assert actions[0].path == '/path/to/file' | |
| assert actions[0].content == 'file content' | |
| assert actions[0].start == 1 | |
| assert actions[0].end == 10 | |
| def test_edit_file_missing_required(): | |
| """Test edit_file with missing required arguments.""" | |
| # Missing path | |
| response = create_mock_response('edit_file', {'content': 'content'}) | |
| with pytest.raises(FunctionCallValidationError) as exc_info: | |
| response_to_actions(response) | |
| assert 'Missing required argument "path"' in str(exc_info.value) | |
| # Missing content | |
| response = create_mock_response('edit_file', {'path': '/path/to/file'}) | |
| with pytest.raises(FunctionCallValidationError) as exc_info: | |
| response_to_actions(response) | |
| assert 'Missing required argument "content"' in str(exc_info.value) | |
| def test_str_replace_editor_valid(): | |
| """Test str_replace_editor with valid arguments.""" | |
| # Test view command | |
| response = create_mock_response( | |
| 'str_replace_editor', {'command': 'view', 'path': '/path/to/file'} | |
| ) | |
| actions = response_to_actions(response) | |
| assert len(actions) == 1 | |
| assert isinstance(actions[0], FileReadAction) | |
| assert actions[0].path == '/path/to/file' | |
| assert actions[0].impl_source == FileReadSource.OH_ACI | |
| # Test other commands | |
| response = create_mock_response( | |
| 'str_replace_editor', | |
| { | |
| 'command': 'str_replace', | |
| 'path': '/path/to/file', | |
| 'old_str': 'old', | |
| 'new_str': 'new', | |
| }, | |
| ) | |
| actions = response_to_actions(response) | |
| assert len(actions) == 1 | |
| assert isinstance(actions[0], FileEditAction) | |
| assert actions[0].path == '/path/to/file' | |
| assert actions[0].impl_source == FileEditSource.OH_ACI | |
| def test_str_replace_editor_missing_required(): | |
| """Test str_replace_editor with missing required arguments.""" | |
| # Missing command | |
| response = create_mock_response('str_replace_editor', {'path': '/path/to/file'}) | |
| with pytest.raises(FunctionCallValidationError) as exc_info: | |
| response_to_actions(response) | |
| assert 'Missing required argument "command"' in str(exc_info.value) | |
| # Missing path | |
| response = create_mock_response('str_replace_editor', {'command': 'view'}) | |
| with pytest.raises(FunctionCallValidationError) as exc_info: | |
| response_to_actions(response) | |
| assert 'Missing required argument "path"' in str(exc_info.value) | |
| def test_browser_valid(): | |
| """Test browser with valid arguments.""" | |
| response = create_mock_response('browser', {'code': "click('button-1')"}) | |
| actions = response_to_actions(response) | |
| assert len(actions) == 1 | |
| assert isinstance(actions[0], BrowseInteractiveAction) | |
| assert actions[0].browser_actions == "click('button-1')" | |
| def test_browser_missing_code(): | |
| """Test browser with missing code argument.""" | |
| response = create_mock_response('browser', {}) | |
| with pytest.raises(FunctionCallValidationError) as exc_info: | |
| response_to_actions(response) | |
| assert 'Missing required argument "code"' in str(exc_info.value) | |
| def test_invalid_json_arguments(): | |
| """Test handling of invalid JSON in arguments.""" | |
| response = ModelResponse( | |
| id='mock-id', | |
| choices=[ | |
| { | |
| 'message': { | |
| 'tool_calls': [ | |
| { | |
| 'function': { | |
| 'name': 'execute_bash', | |
| 'arguments': 'invalid json', | |
| }, | |
| 'id': 'mock-tool-call-id', | |
| 'type': 'function', | |
| } | |
| ], | |
| 'content': None, | |
| 'role': 'assistant', | |
| }, | |
| 'index': 0, | |
| 'finish_reason': 'tool_calls', | |
| } | |
| ], | |
| ) | |
| with pytest.raises(FunctionCallValidationError) as exc_info: | |
| response_to_actions(response) | |
| assert 'Failed to parse tool call arguments' in str(exc_info.value) | |
| def test_unexpected_argument_handling(): | |
| """Test that unexpected arguments in function calls are properly handled. | |
| This test reproduces issue #8369 Example 4 where an unexpected argument | |
| (old_str_prefix) causes a TypeError. | |
| """ | |
| response = create_mock_response( | |
| 'str_replace_editor', | |
| { | |
| 'command': 'str_replace', | |
| 'path': '/test/file.py', | |
| 'old_str': 'def test():\n pass', | |
| 'new_str': 'def test():\n return True', | |
| 'old_str_prefix': 'some prefix', # Unexpected argument | |
| }, | |
| ) | |
| # Test that the function raises a FunctionCallValidationError | |
| with pytest.raises(FunctionCallValidationError) as exc_info: | |
| response_to_actions(response) | |
| # Verify the error message mentions the unexpected argument | |
| assert 'old_str_prefix' in str(exc_info.value) | |
| assert 'Unexpected argument' in str(exc_info.value) | |