mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
8 Commits
remove-unu
...
openhands-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52032a9f91 | ||
|
|
175c17f3bc | ||
|
|
ab950f9baf | ||
|
|
daddc42a9a | ||
|
|
0544660722 | ||
|
|
8943c7ea88 | ||
|
|
2c936a6881 | ||
|
|
d6d24daff7 |
@@ -139,6 +139,13 @@ class ConversationMemory:
|
||||
# and tool responses, for example
|
||||
messages = list(ConversationMemory._filter_unmatched_tool_calls(messages))
|
||||
|
||||
# Filter out assistant messages with empty content lists, unless they have tool calls
|
||||
messages = [
|
||||
msg
|
||||
for msg in messages
|
||||
if msg.role != 'assistant' or msg.content or msg.tool_calls
|
||||
]
|
||||
|
||||
# Apply final formatting
|
||||
messages = self._apply_user_message_formatting(messages)
|
||||
|
||||
@@ -269,7 +276,12 @@ class ConversationMemory:
|
||||
]
|
||||
elif isinstance(action, MessageAction):
|
||||
role = 'user' if action.source == 'user' else 'assistant'
|
||||
content = [TextContent(text=action.content or '')]
|
||||
# Only include content if it's not None and not empty after stripping
|
||||
content = (
|
||||
[TextContent(text=action.content)]
|
||||
if action.content and action.content.strip()
|
||||
else []
|
||||
)
|
||||
if vision_is_active and action.image_urls:
|
||||
content.append(ImageContent(image_urls=action.image_urls))
|
||||
if role not in ('user', 'system', 'assistant', 'tool'):
|
||||
|
||||
@@ -400,6 +400,50 @@ def test_process_events_with_unknown_observation(conversation_memory):
|
||||
)
|
||||
|
||||
|
||||
def test_process_events_with_empty_content_message(conversation_memory):
|
||||
"""Test that empty content messages are handled correctly."""
|
||||
# Create a message action with empty string content
|
||||
empty_message = MessageAction(content='')
|
||||
empty_message._source = EventSource.AGENT
|
||||
|
||||
# Create a message action with None content
|
||||
none_message = MessageAction(content=None)
|
||||
none_message._source = EventSource.AGENT
|
||||
|
||||
# Create a message action with whitespace content
|
||||
whitespace_message = MessageAction(content=' \n \t ')
|
||||
whitespace_message._source = EventSource.AGENT
|
||||
|
||||
# Create a valid message for comparison
|
||||
valid_message = MessageAction(content='Valid content')
|
||||
valid_message._source = EventSource.AGENT
|
||||
|
||||
initial_user_message = MessageAction(content='Initial user message')
|
||||
initial_user_message._source = EventSource.USER
|
||||
|
||||
# Process events
|
||||
messages = conversation_memory.process_events(
|
||||
condensed_history=[
|
||||
empty_message,
|
||||
none_message,
|
||||
whitespace_message,
|
||||
valid_message,
|
||||
],
|
||||
initial_user_action=initial_user_message,
|
||||
max_message_chars=None,
|
||||
vision_is_active=False,
|
||||
)
|
||||
|
||||
# We expect 3 messages: system message, initial user message, and the valid message
|
||||
# The empty, None, and whitespace messages should be filtered out
|
||||
assert len(messages) == 3
|
||||
assert messages[0].role == 'system' # System message
|
||||
assert messages[1].role == 'user' # Initial user message
|
||||
assert messages[2].role == 'assistant' # Valid message
|
||||
assert len(messages[2].content) == 1
|
||||
assert messages[2].content[0].text == 'Valid content'
|
||||
|
||||
|
||||
def test_process_events_with_file_edit_observation(conversation_memory):
|
||||
obs = FileEditObservation(
|
||||
path='/test/file.txt',
|
||||
@@ -1180,10 +1224,121 @@ def test_has_agent_in_earlier_events(conversation_memory):
|
||||
)
|
||||
|
||||
|
||||
def test_process_events_with_empty_content_and_tool_calls(conversation_memory):
|
||||
"""Test that empty string content is handled correctly when tool calls are present.
|
||||
The message should be kept because it has tool calls, even though content is empty."""
|
||||
# Create a tool call for the execute_bash function
|
||||
tool_call = ChatCompletionMessageToolCall(
|
||||
id='test_call_id',
|
||||
function={'name': 'execute_bash', 'arguments': '{"command": "test"}'},
|
||||
type='function',
|
||||
)
|
||||
|
||||
# Create a mock ModelResponse with empty string content but with tool calls
|
||||
mock_response = {
|
||||
'id': 'mock_response_id',
|
||||
'choices': [{'message': {'content': '', 'tool_calls': [tool_call]}}],
|
||||
'created': 0,
|
||||
'model': '',
|
||||
'object': '',
|
||||
'usage': {'completion_tokens': 0, 'prompt_tokens': 0, 'total_tokens': 0},
|
||||
}
|
||||
|
||||
# Create an action with empty content
|
||||
action = CmdRunAction(command='test')
|
||||
action._source = EventSource.AGENT
|
||||
action.tool_call_metadata = ToolCallMetadata(
|
||||
tool_call_id='test_call_id',
|
||||
function_name='execute_bash',
|
||||
model_response=mock_response,
|
||||
total_calls_in_response=1,
|
||||
)
|
||||
|
||||
# Create a mock tool response observation
|
||||
obs = CmdOutputObservation(
|
||||
command='test',
|
||||
content='Command output',
|
||||
command_id=1,
|
||||
exit_code=0,
|
||||
)
|
||||
obs.tool_call_metadata = ToolCallMetadata(
|
||||
tool_call_id='test_call_id',
|
||||
function_name='execute_bash',
|
||||
model_response=mock_response,
|
||||
total_calls_in_response=1,
|
||||
)
|
||||
|
||||
initial_user_message = MessageAction(content='Initial user message')
|
||||
initial_user_message._source = EventSource.USER
|
||||
|
||||
# Process events
|
||||
messages = conversation_memory.process_events(
|
||||
condensed_history=[action, obs], # Include both action and observation
|
||||
initial_user_action=initial_user_message,
|
||||
max_message_chars=None,
|
||||
vision_is_active=False,
|
||||
)
|
||||
|
||||
# Verify that the message with empty content but tool calls is kept
|
||||
assert len(messages) == 4 # System + initial user + assistant with tool call + tool response
|
||||
assert messages[2].role == 'assistant' # Assistant message with tool call
|
||||
assert not messages[2].content # Content should be empty
|
||||
assert messages[2].tool_calls # But tool_calls should be present
|
||||
assert messages[2].tool_calls[0].id == 'test_call_id'
|
||||
assert messages[3].role == 'tool' # Tool response
|
||||
assert messages[3].tool_call_id == 'test_call_id'
|
||||
assert messages[3].content[0].text == 'Command output'
|
||||
|
||||
|
||||
def test_process_events_with_empty_content_no_tool_calls(conversation_memory):
|
||||
"""Test that empty string content is filtered out when there are no tool calls."""
|
||||
# Create a mock ModelResponse with empty string content and no tool calls
|
||||
mock_response = {
|
||||
'id': 'mock_response_id',
|
||||
'choices': [{'message': {'content': '', 'tool_calls': []}}],
|
||||
'created': 0,
|
||||
'model': '',
|
||||
'object': '',
|
||||
'usage': {'completion_tokens': 0, 'prompt_tokens': 0, 'total_tokens': 0},
|
||||
}
|
||||
|
||||
# Create an action with empty content and no tool calls
|
||||
action = MessageAction(content='')
|
||||
action._source = EventSource.AGENT
|
||||
action.tool_call_metadata = ToolCallMetadata(
|
||||
tool_call_id='test_call_id',
|
||||
function_name='execute_bash',
|
||||
model_response=mock_response,
|
||||
total_calls_in_response=0,
|
||||
)
|
||||
|
||||
initial_user_message = MessageAction(content='Initial user message')
|
||||
initial_user_message._source = EventSource.USER
|
||||
|
||||
# Process events
|
||||
messages = conversation_memory.process_events(
|
||||
condensed_history=[action],
|
||||
initial_user_action=initial_user_message,
|
||||
max_message_chars=None,
|
||||
vision_is_active=False,
|
||||
)
|
||||
|
||||
# Verify that the empty content message is filtered out
|
||||
assert len(messages) == 2 # Only system message and initial user message
|
||||
assert messages[0].role == 'system'
|
||||
assert messages[1].role == 'user'
|
||||
assert messages[1].content[0].text == 'Initial user message'
|
||||
|
||||
|
||||
class TestFilterUnmatchedToolCalls:
|
||||
@pytest.fixture
|
||||
def processor(self):
|
||||
return ConversationMemory()
|
||||
def processor(self, agent_config):
|
||||
prompt_manager = MagicMock(spec=PromptManager)
|
||||
prompt_manager.get_system_message.return_value = 'System message'
|
||||
prompt_manager.build_workspace_context.return_value = (
|
||||
'Formatted repository and runtime info'
|
||||
)
|
||||
return ConversationMemory(agent_config, prompt_manager)
|
||||
|
||||
def test_empty_is_unchanged(self):
|
||||
assert list(ConversationMemory._filter_unmatched_tool_calls([])) == []
|
||||
|
||||
Reference in New Issue
Block a user