mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 13:52:43 +08:00
Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
436 lines
16 KiB
Python
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
|