Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issue #5015: [Bug]: Headless mode awaits for requested user feedb… #5246

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion openhands/controller/agent_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class AgentController:
delegate: 'AgentController | None' = None
_pending_action: Action | None = None
_closed: bool = False
fake_user_response_fn: Callable[[str], str] | None = None
filter_out: ClassVar[tuple[type[Event], ...]] = (
NullAction,
NullObservation,
Expand All @@ -89,6 +90,7 @@ def __init__(
is_delegate: bool = False,
headless_mode: bool = True,
status_callback: Callable | None = None,
fake_user_response_fn: Callable[[str], str] | None = None,
):
"""Initializes a new instance of the AgentController class.

Expand All @@ -106,13 +108,21 @@ def __init__(
initial_state: The initial state of the controller.
is_delegate: Whether this controller is a delegate.
headless_mode: Whether the agent is run in headless mode.
fake_user_response_fn: Function to generate fake user responses in headless mode.
If not provided and headless_mode is True, a default function will be used.
status_callback: Optional callback function to handle status updates.
"""
self._step_lock = asyncio.Lock()
self.id = sid
self.agent = agent
self.headless_mode = headless_mode

# Set up default fake user response function for headless mode
if headless_mode and fake_user_response_fn is None:
self.fake_user_response_fn = lambda _: 'continue'
else:
self.fake_user_response_fn = fake_user_response_fn

# subscribe to the event stream
self.event_stream = event_stream
self.event_stream.subscribe(
Expand Down Expand Up @@ -313,7 +323,25 @@ async def _handle_message_action(self, action: MessageAction) -> None:
if self.get_agent_state() != AgentState.RUNNING:
await self.set_agent_state_to(AgentState.RUNNING)
elif action.source == EventSource.AGENT and action.wait_for_response:
await self.set_agent_state_to(AgentState.AWAITING_USER_INPUT)
if self.headless_mode:
# In headless mode, we should use a fake user response if provided
if self.fake_user_response_fn is not None:
response = self.fake_user_response_fn(action.content)
self.event_stream.add_event(
MessageAction(content=response),
EventSource.USER,
)
else:
# If no fake response function is provided, we continue with an empty response
self.event_stream.add_event(
MessageAction(content=''),
EventSource.USER,
)
else:
# Display the message content to help user understand what input is expected
enyst marked this conversation as resolved.
Show resolved Hide resolved
print(f'\nAgent is requesting input: {action.content}')
enyst marked this conversation as resolved.
Show resolved Hide resolved
print('Request user input >> ', end='', flush=True)
enyst marked this conversation as resolved.
Show resolved Hide resolved
await self.set_agent_state_to(AgentState.AWAITING_USER_INPUT)

def reset_task(self) -> None:
"""Resets the agent's task."""
Expand Down
68 changes: 68 additions & 0 deletions tests/unit/test_agent_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,3 +355,71 @@ async def test_step_max_budget_headless(mock_agent, mock_event_stream):
# In headless mode, throttling results in an error
assert controller.state.agent_state == AgentState.ERROR
await controller.close()


@pytest.mark.asyncio
async def test_message_action_user_input_headless(mock_agent, mock_event_stream):
# Test with default fake response
controller = AgentController(
agent=mock_agent,
event_stream=mock_event_stream,
max_iterations=10,
sid='test',
confirmation_mode=False,
headless_mode=True,
)
controller.state.agent_state = AgentState.RUNNING
message_action = MessageAction(content='Test message', wait_for_response=True)
message_action._source = EventSource.AGENT
await controller.on_event(message_action)
# In headless mode with default fake response, should continue running
assert controller.state.agent_state == AgentState.RUNNING
mock_event_stream.add_event.assert_called_once()
args = mock_event_stream.add_event.call_args[0]
assert isinstance(args[0], MessageAction)
assert args[0].content == 'continue'
await controller.close()

# Test with custom fake response
mock_event_stream.reset_mock()
custom_response = 'custom response'
controller = AgentController(
agent=mock_agent,
event_stream=mock_event_stream,
max_iterations=10,
sid='test',
confirmation_mode=False,
headless_mode=True,
fake_user_response_fn=lambda _: custom_response,
)
controller.state.agent_state = AgentState.RUNNING
message_action = MessageAction(content='Test message', wait_for_response=True)
message_action._source = EventSource.AGENT
await controller.on_event(message_action)
# In headless mode with custom fake response, should continue running
assert controller.state.agent_state == AgentState.RUNNING
mock_event_stream.add_event.assert_called_once()
args = mock_event_stream.add_event.call_args[0]
assert isinstance(args[0], MessageAction)
assert args[0].content == custom_response
await controller.close()


@pytest.mark.asyncio
async def test_message_action_user_input_non_headless(mock_agent, mock_event_stream):
controller = AgentController(
agent=mock_agent,
event_stream=mock_event_stream,
max_iterations=10,
sid='test',
confirmation_mode=False,
headless_mode=False,
)
controller.state.agent_state = AgentState.RUNNING
message_action = MessageAction(content='Test message', wait_for_response=True)
message_action._source = EventSource.AGENT
await controller.on_event(message_action)
# In non-headless mode, should wait for user input
assert controller.state.agent_state == AgentState.AWAITING_USER_INPUT
mock_event_stream.add_event.assert_not_called()
enyst marked this conversation as resolved.
Show resolved Hide resolved
await controller.close()
Loading