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) | |