from openhands.events.action import ( Action, AgentFinishAction, AgentRejectAction, BrowseInteractiveAction, BrowseURLAction, CmdRunAction, FileEditAction, FileReadAction, FileWriteAction, MessageAction, RecallAction, ) from openhands.events.action.action import ActionConfirmationStatus from openhands.events.action.files import FileEditSource, FileReadSource from openhands.events.serialization import ( event_from_dict, event_to_dict, ) def serialization_deserialization( original_action_dict, cls, max_message_chars: int = 10000 ): action_instance = event_from_dict(original_action_dict) assert isinstance(action_instance, Action), ( 'The action instance should be an instance of Action.' ) assert isinstance(action_instance, cls), ( f'The action instance should be an instance of {cls.__name__}.' ) # event_to_dict is the regular serialization of an event serialized_action_dict = event_to_dict(action_instance) # it has an extra message property, for the UI serialized_action_dict.pop('message') assert serialized_action_dict == original_action_dict, ( 'The serialized action should match the original action dict.' ) def test_event_props_serialization_deserialization(): original_action_dict = { 'id': 42, 'source': 'agent', 'timestamp': '2021-08-01T12:00:00', 'action': 'message', 'args': { 'content': 'This is a test.', 'image_urls': None, 'wait_for_response': False, }, } serialization_deserialization(original_action_dict, MessageAction) def test_message_action_serialization_deserialization(): original_action_dict = { 'action': 'message', 'args': { 'content': 'This is a test.', 'image_urls': None, 'wait_for_response': False, }, } serialization_deserialization(original_action_dict, MessageAction) def test_agent_finish_action_serialization_deserialization(): original_action_dict = { 'action': 'finish', 'args': { 'outputs': {}, 'thought': '', 'task_completed': None, 'final_thought': '', }, } serialization_deserialization(original_action_dict, AgentFinishAction) def test_agent_reject_action_serialization_deserialization(): original_action_dict = { 'action': 'reject', 'args': {'outputs': {}, 'thought': ''}, } serialization_deserialization(original_action_dict, AgentRejectAction) def test_cmd_run_action_serialization_deserialization(): original_action_dict = { 'action': 'run', 'args': { 'blocking': False, 'command': 'echo "Hello world"', 'is_input': False, 'thought': '', 'hidden': False, 'confirmation_state': ActionConfirmationStatus.CONFIRMED, 'is_static': False, 'cwd': None, }, } serialization_deserialization(original_action_dict, CmdRunAction) def test_browse_url_action_serialization_deserialization(): original_action_dict = { 'action': 'browse', 'args': {'thought': '', 'url': 'https://www.example.com'}, } serialization_deserialization(original_action_dict, BrowseURLAction) def test_browse_interactive_action_serialization_deserialization(): original_action_dict = { 'action': 'browse_interactive', 'args': { 'thought': '', 'browser_actions': 'goto("https://www.example.com")', 'browsergym_send_msg_to_user': '', }, } serialization_deserialization(original_action_dict, BrowseInteractiveAction) def test_file_read_action_serialization_deserialization(): original_action_dict = { 'action': 'read', 'args': { 'path': '/path/to/file.txt', 'start': 0, 'end': -1, 'thought': 'None', 'impl_source': 'default', 'view_range': None, }, } serialization_deserialization(original_action_dict, FileReadAction) def test_file_write_action_serialization_deserialization(): original_action_dict = { 'action': 'write', 'args': { 'path': '/path/to/file.txt', 'content': 'Hello world', 'start': 0, 'end': 1, 'thought': 'None', }, } serialization_deserialization(original_action_dict, FileWriteAction) def test_file_edit_action_aci_serialization_deserialization(): original_action_dict = { 'action': 'edit', 'args': { 'path': '/path/to/file.txt', 'command': 'str_replace', 'file_text': None, 'old_str': 'old text', 'new_str': 'new text', 'insert_line': None, 'content': '', 'start': 1, 'end': -1, 'thought': 'Replacing text', 'impl_source': 'oh_aci', }, } serialization_deserialization(original_action_dict, FileEditAction) def test_file_edit_action_llm_serialization_deserialization(): original_action_dict = { 'action': 'edit', 'args': { 'path': '/path/to/file.txt', 'command': None, 'file_text': None, 'old_str': None, 'new_str': None, 'insert_line': None, 'content': 'Updated content', 'start': 1, 'end': 10, 'thought': 'Updating file content', 'impl_source': 'llm_based_edit', }, } serialization_deserialization(original_action_dict, FileEditAction) def test_cmd_run_action_legacy_serialization(): original_action_dict = { 'action': 'run', 'args': { 'blocking': False, 'command': 'echo "Hello world"', 'thought': '', 'hidden': False, 'confirmation_state': ActionConfirmationStatus.CONFIRMED, 'keep_prompt': False, # will be treated as no-op }, } event = event_from_dict(original_action_dict) assert isinstance(event, Action) assert isinstance(event, CmdRunAction) assert event.command == 'echo "Hello world"' assert event.hidden is False assert not hasattr(event, 'keep_prompt') event_dict = event_to_dict(event) assert 'keep_prompt' not in event_dict['args'] assert ( event_dict['args']['confirmation_state'] == ActionConfirmationStatus.CONFIRMED ) assert event_dict['args']['blocking'] is False assert event_dict['args']['command'] == 'echo "Hello world"' assert event_dict['args']['thought'] == '' assert event_dict['args']['is_input'] is False def test_file_llm_based_edit_action_legacy_serialization(): original_action_dict = { 'action': 'edit', 'args': { 'path': '/path/to/file.txt', 'content': 'dummy content', 'start': 1, 'end': -1, 'thought': 'Replacing text', 'impl_source': 'oh_aci', 'translated_ipython_code': None, }, } event = event_from_dict(original_action_dict) assert isinstance(event, Action) assert isinstance(event, FileEditAction) # Common arguments assert event.path == '/path/to/file.txt' assert event.thought == 'Replacing text' assert event.impl_source == FileEditSource.OH_ACI assert not hasattr(event, 'translated_ipython_code') # OH_ACI arguments assert event.command == '' assert event.file_text is None assert event.old_str is None assert event.new_str is None assert event.insert_line is None # LLM-based editing arguments assert event.content == 'dummy content' assert event.start == 1 assert event.end == -1 event_dict = event_to_dict(event) assert 'translated_ipython_code' not in event_dict['args'] # Common arguments assert event_dict['args']['path'] == '/path/to/file.txt' assert event_dict['args']['impl_source'] == 'oh_aci' assert event_dict['args']['thought'] == 'Replacing text' # OH_ACI arguments assert event_dict['args']['command'] == '' assert event_dict['args']['file_text'] is None assert event_dict['args']['old_str'] is None assert event_dict['args']['new_str'] is None assert event_dict['args']['insert_line'] is None # LLM-based editing arguments assert event_dict['args']['content'] == 'dummy content' assert event_dict['args']['start'] == 1 assert event_dict['args']['end'] == -1 def test_file_ohaci_edit_action_legacy_serialization(): original_action_dict = { 'action': 'edit', 'args': { 'path': '/workspace/game_2048.py', 'content': '', 'start': 1, 'end': -1, 'thought': "I'll help you create a simple 2048 game in Python. I'll use the str_replace_editor to create the file.", 'impl_source': 'oh_aci', 'translated_ipython_code': "print(file_editor(**{'command': 'create', 'path': '/workspace/game_2048.py', 'file_text': 'New file content'}))", }, } event = event_from_dict(original_action_dict) assert isinstance(event, Action) assert isinstance(event, FileEditAction) # Common arguments assert event.path == '/workspace/game_2048.py' assert ( event.thought == "I'll help you create a simple 2048 game in Python. I'll use the str_replace_editor to create the file." ) assert event.impl_source == FileEditSource.OH_ACI assert not hasattr(event, 'translated_ipython_code') # OH_ACI arguments assert event.command == 'create' assert event.file_text == 'New file content' assert event.old_str is None assert event.new_str is None assert event.insert_line is None # LLM-based editing arguments assert event.content == '' assert event.start == 1 assert event.end == -1 event_dict = event_to_dict(event) assert 'translated_ipython_code' not in event_dict['args'] # Common arguments assert event_dict['args']['path'] == '/workspace/game_2048.py' assert event_dict['args']['impl_source'] == 'oh_aci' assert ( event_dict['args']['thought'] == "I'll help you create a simple 2048 game in Python. I'll use the str_replace_editor to create the file." ) # OH_ACI arguments assert event_dict['args']['command'] == 'create' assert event_dict['args']['file_text'] == 'New file content' assert event_dict['args']['old_str'] is None assert event_dict['args']['new_str'] is None assert event_dict['args']['insert_line'] is None # LLM-based editing arguments assert event_dict['args']['content'] == '' assert event_dict['args']['start'] == 1 assert event_dict['args']['end'] == -1 def test_agent_microagent_action_serialization_deserialization(): original_action_dict = { 'action': 'recall', 'args': { 'query': 'What is the capital of France?', 'thought': 'I need to find information about France', 'recall_type': 'knowledge', }, } serialization_deserialization(original_action_dict, RecallAction) def test_file_read_action_legacy_serialization(): original_action_dict = { 'action': 'read', 'args': { 'path': '/workspace/test.txt', 'start': 0, 'end': -1, 'thought': 'Reading the file contents', 'impl_source': 'oh_aci', 'translated_ipython_code': "print(file_editor(**{'command': 'view', 'path': '/workspace/test.txt'}))", }, } event = event_from_dict(original_action_dict) assert isinstance(event, Action) assert isinstance(event, FileReadAction) # Common arguments assert event.path == '/workspace/test.txt' assert event.thought == 'Reading the file contents' assert event.impl_source == FileReadSource.OH_ACI assert not hasattr(event, 'translated_ipython_code') assert not hasattr( event, 'command' ) # FileReadAction should not have command attribute # Read-specific arguments assert event.start == 0 assert event.end == -1 event_dict = event_to_dict(event) assert 'translated_ipython_code' not in event_dict['args'] assert ( 'command' not in event_dict['args'] ) # command should not be in serialized args # Common arguments in serialized form assert event_dict['args']['path'] == '/workspace/test.txt' assert event_dict['args']['impl_source'] == 'oh_aci' assert event_dict['args']['thought'] == 'Reading the file contents' # Read-specific arguments in serialized form assert event_dict['args']['start'] == 0 assert event_dict['args']['end'] == -1