Compare commits

...

12 Commits

Author SHA1 Message Date
openhands
38deae59b6 Fix pr #5246: Fix issue #5015: [Bug]: Headless mode awaits for requested user feedb… 2024-11-29 23:15:47 +00:00
openhands
127636b5ec Fix pr #5246: Fix issue #5015: [Bug]: Headless mode awaits for requested user feedb… 2024-11-29 22:59:58 +00:00
Engel Nyst
200d70d5d5 Update openhands/controller/agent_controller.py 2024-11-26 05:46:11 +01:00
Engel Nyst
da818e24f6 Update openhands/controller/agent_controller.py 2024-11-26 05:45:49 +01:00
Engel Nyst
05dad65bda Update openhands/controller/agent_controller.py 2024-11-26 05:45:29 +01:00
openhands
dfa625ea36 Fix pr #5246: Fix issue #5015: [Bug]: Headless mode awaits for requested user feedb… 2024-11-26 04:38:42 +00:00
Engel Nyst
1687d54f0f Merge branch 'main' into openhands-fix-issue-5015 2024-11-26 05:26:40 +01:00
openhands
d893bea83f Fix pr #5246: Fix issue #5015: [Bug]: Headless mode awaits for requested user feedb… 2024-11-25 04:41:47 +00:00
openhands
7af3f5fb3d Fix pr #5246: Fix issue #5015: [Bug]: Headless mode awaits for requested user feedb… 2024-11-25 04:31:27 +00:00
openhands
41c0c0203f Fix pr #5246: Fix issue #5015: [Bug]: Headless mode awaits for requested user feedb… 2024-11-25 03:40:33 +00:00
openhands
f81c5802b7 Fix pr #5246: Fix issue #5015: [Bug]: Headless mode awaits for requested user feedb… 2024-11-25 00:29:05 +00:00
openhands
2d2729949f Fix issue #5015: [Bug]: Headless mode awaits for requested user feedback without showing any text for what that feedback should be 2024-11-24 23:02:05 +00:00
2 changed files with 101 additions and 1 deletions

View File

@@ -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,
@@ -89,6 +90,7 @@ class AgentController:
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.
@@ -106,6 +108,8 @@ class AgentController:
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()
@@ -113,6 +117,12 @@ class AgentController:
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(
@@ -313,7 +323,22 @@ class AgentController:
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:
await self.set_agent_state_to(AgentState.AWAITING_USER_INPUT)
def reset_task(self) -> None:
"""Resets the agent's task."""

View File

@@ -13,6 +13,7 @@ from openhands.core.schema import AgentState
from openhands.events import Event, EventSource, EventStream, EventStreamSubscriber
from openhands.events.action import ChangeAgentStateAction, CmdRunAction, MessageAction
from openhands.events.observation import (
AgentStateChangedObservation,
ErrorObservation,
)
from openhands.events.serialization import event_to_dict
@@ -355,3 +356,77 @@ 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
# Verify that an AgentStateChangedObservation is added with the correct state
mock_event_stream.add_event.assert_called_once()
args = mock_event_stream.add_event.call_args[0]
assert len(args) == 2
assert isinstance(args[0], AgentStateChangedObservation)
assert args[0].agent_state == AgentState.AWAITING_USER_INPUT
assert args[1] == EventSource.ENVIRONMENT
await controller.close()