OpenHands/tests/unit/test_observation_serialization.py
Rohit Malhotra 0deabd5935
[Feat]: add context msg to new conversation endpoint (#8586)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-05-20 20:47:15 +00:00

436 lines
16 KiB
Python

from openhands.core.schema.observation import ObservationType
from openhands.events.action.files import FileEditSource
from openhands.events.event import RecallType
from openhands.events.observation import (
CmdOutputMetadata,
CmdOutputObservation,
FileEditObservation,
Observation,
RecallObservation,
)
from openhands.events.observation.agent import MicroagentKnowledge
from openhands.events.serialization import (
event_from_dict,
event_to_dict,
event_to_trajectory,
)
from openhands.events.serialization.observation import observation_from_dict
def serialization_deserialization(
original_observation_dict, cls, max_message_chars: int = 10000
):
observation_instance = event_from_dict(original_observation_dict)
assert isinstance(observation_instance, Observation), (
'The observation instance should be an instance of Observation.'
)
assert isinstance(observation_instance, cls), (
f'The observation instance should be an instance of {cls}.'
)
serialized_observation_dict = event_to_dict(observation_instance)
serialized_observation_trajectory = event_to_trajectory(observation_instance)
assert serialized_observation_dict == original_observation_dict, (
'The serialized observation should match the original observation dict.'
)
assert serialized_observation_trajectory == original_observation_dict, (
'The serialized observation trajectory should match the original observation dict.'
)
# Additional tests for various observation subclasses can be included here
def test_observation_event_props_serialization_deserialization():
original_observation_dict = {
'id': 42,
'source': 'agent',
'timestamp': '2021-08-01T12:00:00',
'observation': 'run',
'message': 'Command `ls -l` executed with exit code 0.',
'extras': {
'command': 'ls -l',
'hidden': False,
'metadata': {
'exit_code': 0,
'hostname': None,
'pid': -1,
'prefix': '',
'py_interpreter_path': None,
'suffix': '',
'username': None,
'working_dir': None,
},
},
'content': 'foo.txt',
'success': True,
}
serialization_deserialization(original_observation_dict, CmdOutputObservation)
def test_command_output_observation_serialization_deserialization():
original_observation_dict = {
'observation': 'run',
'extras': {
'command': 'ls -l',
'hidden': False,
'metadata': {
'exit_code': 0,
'hostname': None,
'pid': -1,
'prefix': '',
'py_interpreter_path': None,
'suffix': '',
'username': None,
'working_dir': None,
},
},
'message': 'Command `ls -l` executed with exit code 0.',
'content': 'foo.txt',
'success': True,
}
serialization_deserialization(original_observation_dict, CmdOutputObservation)
def test_success_field_serialization():
# Test success=True
obs = CmdOutputObservation(
content='Command succeeded',
command='ls -l',
metadata=CmdOutputMetadata(
exit_code=0,
),
)
serialized = event_to_dict(obs)
assert serialized['success'] is True
# Test success=False
obs = CmdOutputObservation(
content='No such file or directory',
command='ls -l',
metadata=CmdOutputMetadata(
exit_code=1,
),
)
serialized = event_to_dict(obs)
assert serialized['success'] is False
def test_legacy_serialization():
original_observation_dict = {
'id': 42,
'source': 'agent',
'timestamp': '2021-08-01T12:00:00',
'observation': 'run',
'message': 'Command `ls -l` executed with exit code 0.',
'extras': {
'command': 'ls -l',
'hidden': False,
'exit_code': 0,
'command_id': 3,
},
'content': 'foo.txt',
'success': True,
}
event = event_from_dict(original_observation_dict)
assert isinstance(event, Observation)
assert isinstance(event, CmdOutputObservation)
assert event.metadata.exit_code == 0
assert event.success is True
assert event.command == 'ls -l'
assert event.hidden is False
event_dict = event_to_dict(event)
assert event_dict['success'] is True
assert event_dict['extras']['metadata']['exit_code'] == 0
assert event_dict['extras']['metadata']['pid'] == 3
assert event_dict['extras']['command'] == 'ls -l'
assert event_dict['extras']['hidden'] is False
def test_file_edit_observation_serialization():
original_observation_dict = {
'observation': 'edit',
'extras': {
'_diff_cache': None,
'impl_source': FileEditSource.LLM_BASED_EDIT,
'new_content': None,
'old_content': None,
'path': '',
'prev_exist': False,
'diff': None,
},
'message': 'I edited the file .',
'content': '[Existing file /path/to/file.txt is edited with 1 changes.]',
}
serialization_deserialization(original_observation_dict, FileEditObservation)
def test_file_edit_observation_new_file_serialization():
original_observation_dict = {
'observation': 'edit',
'content': '[New file /path/to/newfile.txt is created with the provided content.]',
'extras': {
'_diff_cache': None,
'impl_source': FileEditSource.LLM_BASED_EDIT,
'new_content': None,
'old_content': None,
'path': '',
'prev_exist': False,
'diff': None,
},
'message': 'I edited the file .',
}
serialization_deserialization(original_observation_dict, FileEditObservation)
def test_file_edit_observation_oh_aci_serialization():
original_observation_dict = {
'observation': 'edit',
'content': 'The file /path/to/file.txt is edited with the provided content.',
'extras': {
'_diff_cache': None,
'impl_source': FileEditSource.LLM_BASED_EDIT,
'new_content': None,
'old_content': None,
'path': '',
'prev_exist': False,
'diff': None,
},
'message': 'I edited the file .',
}
serialization_deserialization(original_observation_dict, FileEditObservation)
def test_file_edit_observation_legacy_serialization():
original_observation_dict = {
'observation': 'edit',
'content': 'content',
'extras': {
'path': '/workspace/game_2048.py',
'prev_exist': False,
'old_content': None,
'new_content': 'new content',
'impl_source': 'oh_aci',
'formatted_output_and_error': 'File created successfully at: /workspace/game_2048.py',
},
}
event = event_from_dict(original_observation_dict)
assert isinstance(event, Observation)
assert isinstance(event, FileEditObservation)
assert event.impl_source == FileEditSource.OH_ACI
assert event.path == '/workspace/game_2048.py'
assert event.prev_exist is False
assert event.old_content is None
assert event.new_content == 'new content'
assert not hasattr(event, 'formatted_output_and_error')
event_dict = event_to_dict(event)
assert event_dict['extras']['impl_source'] == 'oh_aci'
assert event_dict['extras']['path'] == '/workspace/game_2048.py'
assert event_dict['extras']['prev_exist'] is False
assert event_dict['extras']['old_content'] is None
assert event_dict['extras']['new_content'] == 'new content'
assert 'formatted_output_and_error' not in event_dict['extras']
def test_microagent_observation_serialization():
original_observation_dict = {
'observation': 'recall',
'content': '',
'message': 'Added workspace context',
'extras': {
'recall_type': 'workspace_context',
'repo_name': 'some_repo_name',
'repo_directory': 'some_repo_directory',
'runtime_hosts': {'host1': 8080, 'host2': 8081},
'repo_instructions': 'complex_repo_instructions',
'additional_agent_instructions': 'You know it all about this runtime',
'custom_secrets_descriptions': {'SECRET': 'CUSTOM'},
'date': '04/12/1023',
'microagent_knowledge': [],
'conversation_instructions': 'additional_context',
},
}
serialization_deserialization(original_observation_dict, RecallObservation)
def test_microagent_observation_microagent_knowledge_serialization():
original_observation_dict = {
'observation': 'recall',
'content': '',
'message': 'Added microagent knowledge',
'extras': {
'recall_type': 'knowledge',
'repo_name': '',
'repo_directory': '',
'repo_instructions': '',
'runtime_hosts': {},
'additional_agent_instructions': '',
'custom_secrets_descriptions': {},
'conversation_instructions': 'additional_context',
'date': '',
'microagent_knowledge': [
{
'name': 'microagent1',
'trigger': 'trigger1',
'content': 'content1',
},
{
'name': 'microagent2',
'trigger': 'trigger2',
'content': 'content2',
},
],
},
}
serialization_deserialization(original_observation_dict, RecallObservation)
def test_microagent_observation_knowledge_microagent_serialization():
"""Test serialization of a RecallObservation with KNOWLEDGE_MICROAGENT type."""
# Create a RecallObservation with microagent knowledge content
original = RecallObservation(
content='Knowledge microagent information',
recall_type=RecallType.KNOWLEDGE,
microagent_knowledge=[
MicroagentKnowledge(
name='python_best_practices',
trigger='python',
content='Always use virtual environments for Python projects.',
),
MicroagentKnowledge(
name='git_workflow',
trigger='git',
content='Create a new branch for each feature or bugfix.',
),
],
)
# Serialize to dictionary
serialized = event_to_dict(original)
# Verify serialized data structure
assert serialized['observation'] == ObservationType.RECALL
assert serialized['content'] == 'Knowledge microagent information'
assert serialized['extras']['recall_type'] == RecallType.KNOWLEDGE.value
assert len(serialized['extras']['microagent_knowledge']) == 2
assert serialized['extras']['microagent_knowledge'][0]['trigger'] == 'python'
# Deserialize back to RecallObservation
deserialized = observation_from_dict(serialized)
# Verify properties are preserved
assert deserialized.recall_type == RecallType.KNOWLEDGE
assert deserialized.microagent_knowledge == original.microagent_knowledge
assert deserialized.content == original.content
# Check that environment info fields are empty
assert deserialized.repo_name == ''
assert deserialized.repo_directory == ''
assert deserialized.repo_instructions == ''
assert deserialized.runtime_hosts == {}
def test_microagent_observation_environment_serialization():
"""Test serialization of a RecallObservation with ENVIRONMENT type."""
# Create a RecallObservation with environment info
original = RecallObservation(
content='Environment information',
recall_type=RecallType.WORKSPACE_CONTEXT,
repo_name='OpenHands',
repo_directory='/workspace/openhands',
repo_instructions="Follow the project's coding style guide.",
runtime_hosts={'127.0.0.1': 8080, 'localhost': 5000},
additional_agent_instructions='You know it all about this runtime',
)
# Serialize to dictionary
serialized = event_to_dict(original)
# Verify serialized data structure
assert serialized['observation'] == ObservationType.RECALL
assert serialized['content'] == 'Environment information'
assert serialized['extras']['recall_type'] == RecallType.WORKSPACE_CONTEXT.value
assert serialized['extras']['repo_name'] == 'OpenHands'
assert serialized['extras']['runtime_hosts'] == {
'127.0.0.1': 8080,
'localhost': 5000,
}
assert (
serialized['extras']['additional_agent_instructions']
== 'You know it all about this runtime'
)
# Deserialize back to RecallObservation
deserialized = observation_from_dict(serialized)
# Verify properties are preserved
assert deserialized.recall_type == RecallType.WORKSPACE_CONTEXT
assert deserialized.repo_name == original.repo_name
assert deserialized.repo_directory == original.repo_directory
assert deserialized.repo_instructions == original.repo_instructions
assert deserialized.runtime_hosts == original.runtime_hosts
assert (
deserialized.additional_agent_instructions
== original.additional_agent_instructions
)
# Check that knowledge microagent fields are empty
assert deserialized.microagent_knowledge == []
def test_microagent_observation_combined_serialization():
"""Test serialization of a RecallObservation with both types of information."""
# Create a RecallObservation with both environment and microagent info
# Note: In practice, recall_type would still be one specific type,
# but the object could contain both types of fields
original = RecallObservation(
content='Combined information',
recall_type=RecallType.WORKSPACE_CONTEXT,
# Environment info
repo_name='OpenHands',
repo_directory='/workspace/openhands',
repo_instructions="Follow the project's coding style guide.",
runtime_hosts={'127.0.0.1': 8080},
additional_agent_instructions='You know it all about this runtime',
# Knowledge microagent info
microagent_knowledge=[
MicroagentKnowledge(
name='python_best_practices',
trigger='python',
content='Always use virtual environments for Python projects.',
),
],
)
# Serialize to dictionary
serialized = event_to_dict(original)
# Verify serialized data has both types of fields
assert serialized['extras']['recall_type'] == RecallType.WORKSPACE_CONTEXT.value
assert serialized['extras']['repo_name'] == 'OpenHands'
assert (
serialized['extras']['microagent_knowledge'][0]['name']
== 'python_best_practices'
)
assert (
serialized['extras']['additional_agent_instructions']
== 'You know it all about this runtime'
)
# Deserialize back to RecallObservation
deserialized = observation_from_dict(serialized)
# Verify all properties are preserved
assert deserialized.recall_type == RecallType.WORKSPACE_CONTEXT
# Environment properties
assert deserialized.repo_name == original.repo_name
assert deserialized.repo_directory == original.repo_directory
assert deserialized.repo_instructions == original.repo_instructions
assert deserialized.runtime_hosts == original.runtime_hosts
assert (
deserialized.additional_agent_instructions
== original.additional_agent_instructions
)
# Knowledge microagent properties
assert deserialized.microagent_knowledge == original.microagent_knowledge