feat(CLI): Enhance --file option to prompt agent to read and understand file first (#9398)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Xingyao Wang 2025-06-27 11:57:29 -04:00 committed by GitHub
parent 70ad469fb2
commit b74da7d4c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 110 additions and 2 deletions

View File

@ -77,7 +77,6 @@ async def cleanup_session(
controller: AgentController,
) -> None:
"""Clean up all resources from the current session."""
event_stream = runtime.event_stream
end_state = controller.get_state()
end_state.save_to_session(
@ -434,7 +433,23 @@ async def main_with_loop(loop: asyncio.AbstractEventLoop) -> None:
return
# Read task from file, CLI args, or stdin
task_str = read_task(args, config.cli_multiline_input)
if args.file:
# For CLI usage, we want to enhance the file content with a prompt
# that instructs the agent to read and understand the file first
with open(args.file, 'r', encoding='utf-8') as file:
file_content = file.read()
# Create a prompt that instructs the agent to read and understand the file first
task_str = f"""The user has tagged a file '{args.file}'.
Please read and understand the following file content first:
```
{file_content}
```
After reviewing the file, please ask the user what they would like to do with it."""
else:
task_str = read_task(args, config.cli_multiline_input)
# Run the first session
new_session_requested = await run_session(

View File

@ -345,6 +345,7 @@ async def test_main_without_task(
mock_args.agent_cls = None
mock_args.llm_config = None
mock_args.name = None
mock_args.file = None
mock_parse_args.return_value = mock_args
# Mock config
@ -427,6 +428,7 @@ async def test_main_with_task(
mock_args = MagicMock()
mock_args.agent_cls = 'custom-agent'
mock_args.llm_config = 'custom-config'
mock_args.file = None
mock_parse_args.return_value = mock_args
# Mock config
@ -523,6 +525,7 @@ async def test_main_with_session_name_passes_name_to_run_session(
mock_args.agent_cls = None
mock_args.llm_config = None
mock_args.name = test_session_name # Set the session name
mock_args.file = None
mock_parse_args.return_value = mock_args
# Mock config
@ -831,3 +834,93 @@ async def test_config_loading_order(
# Verify that run_session was called with the correct arguments
mock_run_session.assert_called_once()
@pytest.mark.asyncio
@patch('openhands.cli.main.parse_arguments')
@patch('openhands.cli.main.setup_config_from_args')
@patch('openhands.cli.main.FileSettingsStore.get_instance')
@patch('openhands.cli.main.check_folder_security_agreement')
@patch('openhands.cli.main.run_session')
@patch('openhands.cli.main.LLMSummarizingCondenserConfig')
@patch('openhands.cli.main.NoOpCondenserConfig')
@patch('builtins.open', new_callable=MagicMock)
async def test_main_with_file_option(
mock_open,
mock_noop_condenser,
mock_llm_condenser,
mock_run_session,
mock_check_security,
mock_get_settings_store,
mock_setup_config,
mock_parse_args,
):
"""Test main function with a file option."""
loop = asyncio.get_running_loop()
# Mock arguments
mock_args = MagicMock()
mock_args.agent_cls = None
mock_args.llm_config = None
mock_args.name = None
mock_args.file = '/path/to/test/file.txt'
mock_args.task = None
mock_parse_args.return_value = mock_args
# Mock config
mock_config = MagicMock()
mock_config.workspace_base = '/test/dir'
mock_config.cli_multiline_input = False
mock_setup_config.return_value = mock_config
# Mock settings store
mock_settings_store = AsyncMock()
mock_settings = MagicMock()
mock_settings.agent = 'test-agent'
mock_settings.llm_model = 'test-model'
mock_settings.llm_api_key = 'test-api-key'
mock_settings.llm_base_url = 'test-base-url'
mock_settings.confirmation_mode = True
mock_settings.enable_default_condenser = True
mock_settings_store.load.return_value = mock_settings
mock_get_settings_store.return_value = mock_settings_store
# Mock condenser config to return a mock instead of validating
mock_llm_condenser_instance = MagicMock()
mock_llm_condenser.return_value = mock_llm_condenser_instance
# Mock security check
mock_check_security.return_value = True
# Mock file open
mock_file = MagicMock()
mock_file.__enter__.return_value.read.return_value = 'This is a test file content.'
mock_open.return_value = mock_file
# Mock run_session to return False (no new session requested)
mock_run_session.return_value = False
# Run the function
await cli.main_with_loop(loop)
# Assertions
mock_parse_args.assert_called_once()
mock_setup_config.assert_called_once_with(mock_args)
mock_get_settings_store.assert_called_once()
mock_settings_store.load.assert_called_once()
mock_check_security.assert_called_once_with(mock_config, '/test/dir')
# Verify file was opened
mock_open.assert_called_once_with('/path/to/test/file.txt', 'r', encoding='utf-8')
# Check that run_session was called with expected arguments
mock_run_session.assert_called_once()
# Extract the task_str from the call
task_str = mock_run_session.call_args[0][4]
assert "The user has tagged a file '/path/to/test/file.txt'" in task_str
assert 'Please read and understand the following file content first:' in task_str
assert 'This is a test file content.' in task_str
assert (
'After reviewing the file, please ask the user what they would like to do with it.'
in task_str
)