forked from All-Hands-AI/OpenHands
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Improve function call validation with better error handling (All-Hand…
…s-AI#6453) Co-authored-by: openhands <[email protected]>
- Loading branch information
Showing
2 changed files
with
265 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
"""Test function calling module.""" | ||
|
||
import json | ||
|
||
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, | ||
BrowseURLAction, | ||
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 | ||
|
||
|
||
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_web_read_valid(): | ||
"""Test web_read with valid arguments.""" | ||
response = create_mock_response('web_read', {'url': 'https://example.com'}) | ||
actions = response_to_actions(response) | ||
assert len(actions) == 1 | ||
assert isinstance(actions[0], BrowseURLAction) | ||
assert actions[0].url == 'https://example.com' | ||
|
||
|
||
def test_web_read_missing_url(): | ||
"""Test web_read with missing url argument.""" | ||
response = create_mock_response('web_read', {}) | ||
with pytest.raises(FunctionCallValidationError) as exc_info: | ||
response_to_actions(response) | ||
assert 'Missing required argument "url"' 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(RuntimeError) as exc_info: | ||
response_to_actions(response) | ||
assert 'Failed to parse tool call arguments' in str(exc_info.value) |