From 76c992e2dfb424ea52cc7270bd9ff4e78b8f1d2d Mon Sep 17 00:00:00 2001 From: Rohit Malhotra Date: Thu, 27 Mar 2025 13:58:37 -0400 Subject: [PATCH] [Feat]: Trigger microagents on agent keywords (#7516) --- openhands/controller/agent_controller.py | 14 +++++- openhands/memory/memory.py | 9 ++-- tests/unit/test_memory.py | 58 ++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/openhands/controller/agent_controller.py b/openhands/controller/agent_controller.py index 2c91809cec..43c6250bdb 100644 --- a/openhands/controller/agent_controller.py +++ b/openhands/controller/agent_controller.py @@ -479,8 +479,18 @@ 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) + elif action.source == EventSource.AGENT: + # Check if we need to trigger microagents based on agent message content + recall_action = RecallAction( + query=action.content, recall_type=RecallType.KNOWLEDGE + ) + self._pending_action = recall_action + # This is source=AGENT because the agent message is the trigger for the microagent retrieval + self.event_stream.add_event(recall_action, EventSource.AGENT) + + # If the agent is waiting for a response, set the appropriate state + if action.wait_for_response: + await self.set_agent_state_to(AgentState.AWAITING_USER_INPUT) def _reset(self) -> None: """Resets the agent controller.""" diff --git a/openhands/memory/memory.py b/openhands/memory/memory.py index aacfe1bca7..948aefede3 100644 --- a/openhands/memory/memory.py +++ b/openhands/memory/memory.py @@ -98,11 +98,14 @@ class Memory: return # Handle knowledge recall (triggered microagents) + # Allow triggering from both user and agent messages elif ( event.source == EventSource.USER - and event.recall_type == RecallType.KNOWLEDGE - ): - logger.debug('Microagent knowledge recall') + or event.source == EventSource.AGENT + ) and event.recall_type == RecallType.KNOWLEDGE: + logger.debug( + f'Microagent knowledge recall from {event.source} message' + ) microagent_obs: RecallObservation | NullObservation | None = None microagent_obs = self._on_microagent_recall(event) if microagent_obs is None: diff --git a/tests/unit/test_memory.py b/tests/unit/test_memory.py index 663d528eb1..83e7dfc360 100644 --- a/tests/unit/test_memory.py +++ b/tests/unit/test_memory.py @@ -261,3 +261,61 @@ REPOSITORY INSTRUCTIONS: This is a test repository. # Clean up os.remove(os.path.join(prompt_dir, 'micro', f'{repo_microagent_name}.md')) + + +@pytest.mark.asyncio +async def test_memory_with_agent_microagents(): + """ + Test that Memory processes microagent based on trigger words from agent messages. + """ + # Create a mock event stream + event_stream = MagicMock(spec=EventStream) + + # Initialize Memory to use the global microagents dir + memory = Memory( + event_stream=event_stream, + sid='test-session', + ) + + # Verify microagents were loaded - at least one microagent should be loaded + # from the global directory that's in the repo + assert len(memory.knowledge_microagents) > 0 + + # We know 'flarglebargle' exists in the global directory + assert 'flarglebargle' in memory.knowledge_microagents + + # Create a microagent action with the trigger word + microagent_action = RecallAction( + query='Hello, flarglebargle!', recall_type=RecallType.KNOWLEDGE + ) + + # Set the source to AGENT + microagent_action._source = EventSource.AGENT # type: ignore[attr-defined] + + # Mock the event_stream.add_event method + added_events = [] + + def original_add_event(event, source): + added_events.append((event, source)) + + event_stream.add_event = original_add_event + + # Add the microagent action to the event stream + event_stream.add_event(microagent_action, EventSource.AGENT) + + # Clear the events list to only capture new events + added_events.clear() + + # Process the microagent action + await memory._on_event(microagent_action) + + # Verify a RecallObservation was added to the event stream + assert len(added_events) == 1 + observation, source = added_events[0] + assert isinstance(observation, RecallObservation) + assert source == EventSource.ENVIRONMENT + assert observation.recall_type == RecallType.KNOWLEDGE + assert len(observation.microagent_knowledge) == 1 + assert observation.microagent_knowledge[0].name == 'flarglebargle' + assert observation.microagent_knowledge[0].trigger == 'flarglebargle' + assert 'magic word' in observation.microagent_knowledge[0].content