diff --git a/agents/sirji_agents/llm/generic/infer.py b/agents/sirji_agents/llm/generic/infer.py index c300a9f..8a04ddb 100644 --- a/agents/sirji_agents/llm/generic/infer.py +++ b/agents/sirji_agents/llm/generic/infer.py @@ -1,5 +1,5 @@ from sirji_tools.logger import create_logger -from sirji_messages import message_parse, MessageParsingError, MessageValidationError, ActionEnum, AgentEnum, allowed_response_templates, permissions_dict, ActionEnum +from sirji_messages import message_parse, ActionEnum, AgentEnum, allowed_response_templates, permissions_dict, ActionEnum, MessageIncorrectFormatError, MessageMultipleActionError, MessageUnRecognizedActionError, MessageMissingPropertyError, MessageLengthConstraintError from ..model_providers.factory import LLMProviderFactory from .system_prompts.factory import SystemPromptsFactory from ...decorators import retry_on_exception @@ -58,16 +58,27 @@ def __get_response(self, conversation): parsed_response_message = message_parse(response_message) conversation.append({"role": "assistant", "content": response_message, "parsed_content": parsed_response_message}) break - except (MessageParsingError, MessageValidationError) as e: - # Handling both MessageParsingError and MessageValidationError similarly - self.logger.info("Error while parsing the message.\n") - retry_llm_count += 1 - if retry_llm_count > 2: - raise e - self.logger.info(f"Requesting LLM to resend the message in correct format.\n") - conversation.append({"role": "assistant", "content": response_message, "parsed_content": {}}) - # Todo: @vaibhav - Change the error message language later. - conversation.append({"role": "user", "content": "Error! Your last response has two action in it and both has been discarded because of the below error:\nError in processing your last response. Your response must conform strictly to one of the allowed Response Templates, as it will be processed programmatically and only these templates are recognized. Your response must be enclosed within '***' at the beginning and end, without any additional text above or below these markers. Not conforming above rules will lead to response processing errors."}) + except (MessageIncorrectFormatError, MessageMultipleActionError, MessageUnRecognizedActionError, MessageMissingPropertyError, MessageLengthConstraintError ) as e: + if isinstance(e, MessageIncorrectFormatError): + self.logger.info("Error while parsing the message.\n") + retry_llm_count += 1 + if retry_llm_count > 2: + raise e + self.logger.info(f"Requesting LLM to resend the message in correct format.\n") + conversation.append({"role": "assistant", "content": response_message, "parsed_content": {}}) + conversation.append({"role": "user", "content": "Error obtained in processing your last response. Your response must conform strictly to one of the allowed Response Templates, as it will be processed programmatically and only these templates are recognized. Your response must be enclosed within '***' at the beginning and end, without any additional text above or below these markers. Not conforming above rules will lead to response processing errors."}) + if isinstance(e, MessageMissingPropertyError): + conversation.append({"role": "assistant", "content": response_message, "parsed_content": {}}) + conversation.append({"role": "user", "content": "Error obtained in processing your last response. your response is missing some required properties. Please refer to the allowed response templates and provide the missing properties."}) + if isinstance(e, MessageUnRecognizedActionError): + conversation.append({"role": "assistant", "content": response_message, "parsed_content": {}}) + conversation.append({"role": "user", "content": "Error obtained in processing your last response. Your response contains an unrecognized action. Please refer to the allowed response templates and provide a valid action."}) + if isinstance(e, MessageLengthConstraintError): + conversation.append({"role": "assistant", "content": response_message, "parsed_content": {}}) + conversation.append({"role": "user", "content": "Error obtained in processing your last response. Your response does not have all the required properties."}) + if isinstance(e, MessageMultipleActionError): + conversation.append({"role": "assistant", "content": response_message, "parsed_content": {}}) + conversation.append({"role": "user", "content": "Error obtained in processing your last response. Your response contains more than one ACTION keyword."}) except Exception as e: self.logger.info(f"Generic error while parsing message. Error: {e}\n") raise e diff --git a/agents/sirji_agents/llm/orchestrator.py b/agents/sirji_agents/llm/orchestrator.py index a948e07..1a1ad93 100644 --- a/agents/sirji_agents/llm/orchestrator.py +++ b/agents/sirji_agents/llm/orchestrator.py @@ -5,7 +5,7 @@ # TODO - log file should be dynamically created based on agent ID from sirji_tools.logger import o_logger as logger -from sirji_messages import message_parse, MessageParsingError, MessageValidationError, ActionEnum, AgentEnum, allowed_response_templates, permissions_dict +from sirji_messages import message_parse, ActionEnum, AgentEnum, allowed_response_templates, permissions_dict, MessageIncorrectFormatError, MessageMultipleActionError, MessageUnRecognizedActionError, MessageMissingPropertyError, MessageLengthConstraintError from .model_providers.factory import LLMProviderFactory from ..decorators import retry_on_exception @@ -57,15 +57,27 @@ def __get_response(self, conversation): parsed_response_message = message_parse(response_message) conversation.append({"role": "assistant", "content": response_message, "parsed_content": parsed_response_message}) break - except (MessageParsingError, MessageValidationError) as e: - # Handling both MessageParsingError and MessageValidationError similarly - logger.info("Error while parsing the message.\n") - retry_llm_count += 1 - if retry_llm_count > 2: - raise e - logger.info(f"Requesting LLM to resend the message in correct format.\n") - conversation.append({"role": "assistant", "content": response_message, "parsed_content": {}}) - conversation.append({"role": "user", "content": "Error obtained in processing your last response. Your response must conform strictly to one of the allowed Response Templates, as it will be processed programmatically and only these templates are recognized. Your response must be enclosed within '***' at the beginning and end, without any additional text above or below these markers. Not conforming above rules will lead to response processing errors."}) + except (MessageIncorrectFormatError, MessageMultipleActionError, MessageUnRecognizedActionError, MessageMissingPropertyError, MessageLengthConstraintError ) as e: + if isinstance(e, MessageIncorrectFormatError): + logger.info("Error while parsing the message.\n") + retry_llm_count += 1 + if retry_llm_count > 2: + raise e + logger.info(f"Requesting LLM to resend the message in correct format.\n") + conversation.append({"role": "assistant", "content": response_message, "parsed_content": {}}) + conversation.append({"role": "user", "content": "Error obtained in processing your last response. Your response must conform strictly to one of the allowed Response Templates, as it will be processed programmatically and only these templates are recognized. Your response must be enclosed within '***' at the beginning and end, without any additional text above or below these markers. Not conforming above rules will lead to response processing errors."}) + if isinstance(e, MessageMissingPropertyError): + conversation.append({"role": "assistant", "content": response_message, "parsed_content": {}}) + conversation.append({"role": "user", "content": "Error obtained in processing your last response. your response is missing some required properties. Please refer to the allowed response templates and provide the missing properties."}) + if isinstance(e, MessageUnRecognizedActionError): + conversation.append({"role": "assistant", "content": response_message, "parsed_content": {}}) + conversation.append({"role": "user", "content": "Error obtained in processing your last response. Your response contains an unrecognized action. Please refer to the allowed response templates and provide a valid action."}) + if isinstance(e, MessageLengthConstraintError): + conversation.append({"role": "assistant", "content": response_message, "parsed_content": {}}) + conversation.append({"role": "user", "content": "Error obtained in processing your last response. Your response does not have all the required properties."}) + if isinstance(e, MessageMultipleActionError): + conversation.append({"role": "assistant", "content": response_message, "parsed_content": {}}) + conversation.append({"role": "user", "content": "Error obtained in processing your last response. Your response contains more than one ACTION keyword."}) except Exception as e: logger.info(f"Generic error while parsing message. Error: {e}\n") raise e diff --git a/messages/README.md b/messages/README.md index 1bd37bc..e52c806 100644 --- a/messages/README.md +++ b/messages/README.md @@ -71,7 +71,7 @@ message_str = message_class().generate({ "to_agent_id": "CODER", "summary": "Empty", "body": "Done."}) - + print(f"Generated RESPONSE message:\n{message_str}") ``` @@ -79,7 +79,7 @@ print(f"Generated RESPONSE message:\n{message_str}") Parse structured message strings into Python dictionaries for easy access to the message components. -````python +```python from sirji_messages import message_parse # Example message string to parse @@ -94,7 +94,7 @@ BODY: Welcome to sirji-messages. Here's how you can start. # Parsing the message message = message_parse(message_str) print(message) -```` +``` ### Allowed Response Templates @@ -113,15 +113,15 @@ print(response_templates_str) Efficiently manage parsing and validation errors with custom exceptions for improved error handling and debugging. ```python -from sirji_messages import MessageParsingError, MessageValidationError, message_parse +from sirji_messages import MessageIncorrectFormatError, MessageMultipleActionError, MessageUnRecognizedActionError, MessageMissingPropertyError, MessageLengthConstraintError, message_parse try: # Attempt parsing an incorrectly formatted message message_parse("INCORRECT_FORMAT") except MessageParsingError as e: print(f"Parsing Error: {e}") -except MessageValidationError as e: - print(f"Validation Error: {e}") +except MessageIncorrectFormatError as e: + print(f"Incorrect Format Error: {e}") ``` ### Enums for Agents and Actions diff --git a/messages/sirji_messages/__init__.py b/messages/sirji_messages/__init__.py index 8c617bf..60b78e8 100644 --- a/messages/sirji_messages/__init__.py +++ b/messages/sirji_messages/__init__.py @@ -1,7 +1,7 @@ from .action_enum import ActionEnum from .agent_enum import AgentEnum from .messages.factory import MessageFactory -from .custom_exceptions import MessageParsingError, MessageValidationError +from .custom_exceptions import MessageIncorrectFormatError, MessageMultipleActionError, MessageUnRecognizedActionError, MessageMissingPropertyError, MessageLengthConstraintError from .permissions import validate_permission, permissions_dict from .parser import parse as message_parse from .helper import allowed_response_templates @@ -12,9 +12,13 @@ 'MessageFactory', 'ActionEnum', 'AgentEnum', - 'MessageParsingError', - 'MessageValidationError', 'validate_permission', 'permissions_dict', - 'allowed_response_templates' + 'allowed_response_templates', + 'MessageIncorrectFormatError', + 'MessageMultipleActionError', + 'MessageUnRecognizedActionError', + 'MessageMissingPropertyError', + 'MessageLengthConstraintError', ] + diff --git a/messages/sirji_messages/custom_exceptions.py b/messages/sirji_messages/custom_exceptions.py index 01e2038..97aece1 100644 --- a/messages/sirji_messages/custom_exceptions.py +++ b/messages/sirji_messages/custom_exceptions.py @@ -1,14 +1,34 @@ -class MessageValidationError(Exception): +class MessageMissingPropertyError(Exception): """Exception raised for errors in the input message format.""" - def __init__(self, message="Invalid message format"): + def __init__(self, message="Message does not contain required property"): self.message = message super().__init__(self.message) +class MessageUnRecognizedActionError(Exception): + """Exception raised for errors in the input message format.""" + + def __init__(self, message="Action is not recognized"): + self.message = message + super().__init__(self.message) + +class MessageIncorrectFormatError(Exception): + """Exception raised for errors in the input message format.""" + + def __init__(self, message="Message must start and end with ***"): + self.message = message + super().__init__(self.message) -class MessageParsingError(Exception): - """Exception raised for errors in parsing the message contents.""" +class MessageLengthConstraintError(Exception): + """Exception raised for errors in the input message format.""" - def __init__(self, message="Error parsing message"): + def __init__(self, message="Message does not meet the minimum length requirement"): self.message = message super().__init__(self.message) + +class MessageMultipleActionError(Exception): + """Exception raised for errors in the input message format.""" + + def __init__(self, message="Message contains more than one ACTION keyword"): + self.message = message + super().__init__(self.message) \ No newline at end of file diff --git a/messages/sirji_messages/parser.py b/messages/sirji_messages/parser.py index 249f083..c116afc 100644 --- a/messages/sirji_messages/parser.py +++ b/messages/sirji_messages/parser.py @@ -1,4 +1,4 @@ -from .custom_exceptions import MessageParsingError, MessageValidationError +from .custom_exceptions import MessageIncorrectFormatError, MessageMultipleActionError, MessageUnRecognizedActionError, MessageMissingPropertyError, MessageLengthConstraintError from .action_enum import ActionEnum message_properties = ['FROM', 'TO', 'ACTION','STEP', 'SUMMARY', 'BODY'] @@ -11,10 +11,10 @@ def parse(input_message): # Check if the message has all the required properties. for prop in message_properties: if prop not in parsed_message: - raise MessageParsingError(f"Message does not contain {prop} property") + raise MessageMissingPropertyError(f"Message does not contain {prop} property") if parsed_message['ACTION'] not in ActionEnum.__members__: - raise MessageValidationError(f"Action {parsed_message['ACTION']} is not recognized") + raise MessageUnRecognizedActionError(f"Action {parsed_message['ACTION']} is not recognized") return parsed_message @@ -38,11 +38,13 @@ def _discard_format_deviations(input_message): def _validate_message(message): message = message.strip() if not (message.startswith("***") and message.endswith("***")): - raise MessageValidationError("Message must start and end with ***") + raise MessageIncorrectFormatError("Message must start and end with ***") + if message.count("ACTION") > 1: + raise MessageMultipleActionError("Message contains more than one ACTION keyword") message = message[3:-3].strip() lines = message.split("\n") if len(lines) < max(len(message_properties), 2): - raise MessageValidationError( + raise MessageLengthConstraintError( "Message does not meet the minimum length requirement") return lines diff --git a/messages/tests/test_custom_exceptions.py b/messages/tests/test_custom_exceptions.py index da4d870..1bf4fed 100644 --- a/messages/tests/test_custom_exceptions.py +++ b/messages/tests/test_custom_exceptions.py @@ -1,26 +1,63 @@ import pytest -from sirji_messages.custom_exceptions import MessageValidationError, MessageParsingError - -def test_message_validation_error(): - with pytest.raises(MessageValidationError) as exc_info: - raise MessageValidationError("Test validation error") - assert str(exc_info.value) == "Test validation error" - assert exc_info.value.message == "Test validation error" - -def test_message_validation_error_default_message(): - with pytest.raises(MessageValidationError) as exc_info: - raise MessageValidationError() - assert str(exc_info.value) == "Invalid message format" - assert exc_info.value.message == "Invalid message format" - -def test_message_parsing_error(): - with pytest.raises(MessageParsingError) as exc_info: - raise MessageParsingError("Test parsing error") - assert str(exc_info.value) == "Test parsing error" - assert exc_info.value.message == "Test parsing error" - -def test_message_parsing_error_default_message(): - with pytest.raises(MessageParsingError) as exc_info: - raise MessageParsingError() - assert str(exc_info.value) == "Error parsing message" - assert exc_info.value.message == "Error parsing message" \ No newline at end of file +from sirji_messages.custom_exceptions import MessageIncorrectFormatError, MessageMultipleActionError, MessageUnRecognizedActionError, MessageMissingPropertyError, MessageLengthConstraintError + + +def test_message_incorrect_format_error(): + with pytest.raises(MessageIncorrectFormatError) as exc_info: + raise MessageIncorrectFormatError("Test incorrect format error") + assert str(exc_info.value) == "Test incorrect format error" + assert exc_info.value.message == "Test incorrect format error" + +def test_message_incorrect_format_error_default_message(): + with pytest.raises(MessageIncorrectFormatError) as exc_info: + raise MessageIncorrectFormatError() + assert str(exc_info.value) == "Message must start and end with ***" + assert exc_info.value.message == "Message must start and end with ***" + +def test_message_multiple_action_error(): + with pytest.raises(MessageMultipleActionError) as exc_info: + raise MessageMultipleActionError("Test multiple action error") + assert str(exc_info.value) == "Test multiple action error" + assert exc_info.value.message == "Test multiple action error" + +def test_message_multiple_action_error_default_message(): + with pytest.raises(MessageMultipleActionError) as exc_info: + raise MessageMultipleActionError() + assert str(exc_info.value) == "Message contains more than one ACTION keyword" + assert exc_info.value.message == "Message contains more than one ACTION keyword" + +def test_message_unrecognized_action_error(): + with pytest.raises(MessageUnRecognizedActionError) as exc_info: + raise MessageUnRecognizedActionError("Test unrecognized action error") + assert str(exc_info.value) == "Test unrecognized action error" + assert exc_info.value.message == "Test unrecognized action error" + +def test_message_unrecognized_action_error_default_message(): + with pytest.raises(MessageUnRecognizedActionError) as exc_info: + raise MessageUnRecognizedActionError() + assert str(exc_info.value) == "Action is not recognized" + assert exc_info.value.message == "Action is not recognized" + +def test_message_missing_property_error(): + with pytest.raises(MessageMissingPropertyError) as exc_info: + raise MessageMissingPropertyError("Test missing property error") + assert str(exc_info.value) == "Test missing property error" + assert exc_info.value.message == "Test missing property error" + +def test_message_missing_property_error_default_message(): + with pytest.raises(MessageMissingPropertyError) as exc_info: + raise MessageMissingPropertyError() + assert str(exc_info.value) == "Message does not contain required property" + assert exc_info.value.message == "Message does not contain required property" + +def test_message_length_constraint_error(): + with pytest.raises(MessageLengthConstraintError) as exc_info: + raise MessageLengthConstraintError("Test length constraint error") + assert str(exc_info.value) == "Test length constraint error" + assert exc_info.value.message == "Test length constraint error" + +def test_message_length_constraint_error_default_message(): + with pytest.raises(MessageLengthConstraintError) as exc_info: + raise MessageLengthConstraintError() + assert str(exc_info.value) == "Message does not meet the minimum length requirement" + assert exc_info.value.message == "Message does not meet the minimum length requirement" \ No newline at end of file diff --git a/messages/tests/test_init.py b/messages/tests/test_init.py index 72fb106..40ec662 100644 --- a/messages/tests/test_init.py +++ b/messages/tests/test_init.py @@ -5,8 +5,11 @@ MessageFactory, ActionEnum, AgentEnum, - MessageParsingError, - MessageValidationError, + MessageIncorrectFormatError, + MessageMultipleActionError, + MessageUnRecognizedActionError, + MessageMissingPropertyError, + MessageLengthConstraintError, validate_permission, permissions_dict, allowed_response_templates @@ -18,8 +21,11 @@ def test_imports(): assert MessageFactory is not None assert ActionEnum is not None assert AgentEnum is not None - assert MessageParsingError is not None - assert MessageValidationError is not None + assert MessageIncorrectFormatError is not None + assert MessageMissingPropertyError is not None + assert MessageLengthConstraintError is not None + assert MessageMultipleActionError is not None + assert MessageUnRecognizedActionError is not None assert validate_permission is not None assert permissions_dict is not None assert allowed_response_templates is not None @@ -31,11 +37,30 @@ def test_enum_members(): def test_exceptions(): # Test if exceptions can be raised and caught - with pytest.raises(MessageParsingError): - raise MessageParsingError("Test parsing error") - - with pytest.raises(MessageValidationError): - raise MessageValidationError("Test validation error") + with pytest.raises(MessageIncorrectFormatError) as exc_info: + raise MessageIncorrectFormatError("Test incorrect format error") + assert str(exc_info.value) == "Test incorrect format error" + assert exc_info.value.message == "Test incorrect format error" + + with pytest.raises(MessageMultipleActionError) as exc_info: + raise MessageMultipleActionError("Test multiple action error") + assert str(exc_info.value) == "Test multiple action error" + assert exc_info.value.message == "Test multiple action error" + + with pytest.raises(MessageUnRecognizedActionError) as exc_info: + raise MessageUnRecognizedActionError("Test unrecognized action error") + assert str(exc_info.value) == "Test unrecognized action error" + assert exc_info.value.message == "Test unrecognized action error" + + with pytest.raises(MessageMissingPropertyError) as exc_info: + raise MessageMissingPropertyError("Test missing property error") + assert str(exc_info.value) == "Test missing property error" + assert exc_info.value.message == "Test missing property error" + + with pytest.raises(MessageLengthConstraintError) as exc_info: + raise MessageLengthConstraintError("Test length constraint error") + assert str(exc_info.value) == "Test length constraint error" + assert exc_info.value.message == "Test length constraint error" def test_validate_permission(): # Test if validate_permission function works as expected diff --git a/messages/tests/test_parser.py b/messages/tests/test_parser.py index 9393c4f..7b32333 100644 --- a/messages/tests/test_parser.py +++ b/messages/tests/test_parser.py @@ -1,6 +1,12 @@ - import pytest -from sirji_messages.parser import parse, MessageParsingError, MessageValidationError +from sirji_messages.parser import parse +from sirji_messages.custom_exceptions import ( + MessageIncorrectFormatError, + MessageMultipleActionError, + MessageLengthConstraintError, +) +from sirji_messages.action_enum import ActionEnum + def test_parse_valid_message(): message_str = """*** @@ -11,66 +17,138 @@ def test_parse_valid_message(): SUMMARY: Welcome BODY: Welcome to sirji-messages. Here's how you can start. ***""" - + expected_output = { - 'FROM': 'CODER', - 'TO': 'USER', - 'ACTION': 'INFORM', - 'STEP': 'step1', - 'SUMMARY': 'Welcome', - 'BODY': "Welcome to sirji-messages. Here's how you can start." + "FROM": "CODER", + "TO": "USER", + "ACTION": "INFORM", + "STEP": "step1", + "SUMMARY": "Welcome", + "BODY": "Welcome to sirji-messages. Here's how you can start.", } - + parsed_message = parse(message_str) assert parsed_message == expected_output -def test_parse_missing_property(): - message_str = """*** + +def test_parse_invalid_format(): + message_str = """** FROM: CODER TO: USER ACTION: INFORM + STEP: step1 SUMMARY: Welcome + BODY: Welcome to sirji-messages. Here's how you can start. + **""" + + with pytest.raises(MessageIncorrectFormatError) as exc_info: + parse(message_str) + assert "Message must start and end with ***" in str(exc_info.value) + + +def test_parse_short_message(): + message_str = """*** + FROM: CODER + TO: USER + ACTION: INFORM ***""" - - with pytest.raises(MessageValidationError) as exc_info: + + with pytest.raises(MessageLengthConstraintError) as exc_info: parse(message_str) - assert str(exc_info.value) == "Message does not meet the minimum length requirement" + assert "Message does not meet the minimum length requirement" in str(exc_info.value) -def test_parse_invalid_action(): + +def test_parse_multiple_action(): message_str = """*** FROM: CODER TO: USER - ACTION: INVALID_ACTION + ACTION: INFORM + ACTION: ACTION_TOO_MANY STEP: step1 SUMMARY: Welcome BODY: Welcome to sirji-messages. Here's how you can start. ***""" - - with pytest.raises(MessageValidationError) as exc_info: + + with pytest.raises(MessageMultipleActionError) as exc_info: parse(message_str) - assert str(exc_info.value) == "Action INVALID_ACTION is not recognized" + assert "Message contains more than one ACTION keyword" in str(exc_info.value) -def test_parse_invalid_format(): - message_str = """** + +# Remaining test cases + +def test_parse_message_with_minimal_body(): + message_str = """*** FROM: CODER TO: USER ACTION: INFORM STEP: step1 SUMMARY: Welcome - BODY: Welcome to sirji-messages. Here's how you can start. - **""" - - with pytest.raises(MessageValidationError) as exc_info: - parse(message_str) - assert str(exc_info.value) == "Message must start and end with ***" + BODY: Welcome. + ***""" -def test_parse_short_message(): + expected_output = { + "FROM": "CODER", + "TO": "USER", + "ACTION": "INFORM", + "STEP": "step1", + "SUMMARY": "Welcome", + "BODY": "Welcome.", + } + + parsed_message = parse(message_str) + + for key in expected_output: + assert parsed_message[key].strip() == expected_output[key].strip() + + +def test_parse_message_with_empty_body(): message_str = """*** FROM: CODER TO: USER ACTION: INFORM + STEP: step1 + SUMMARY: Welcome + BODY: ***""" - - with pytest.raises(MessageValidationError) as exc_info: - parse(message_str) - assert str(exc_info.value) == "Message does not meet the minimum length requirement" \ No newline at end of file + + expected_output = { + "FROM": "CODER", + "TO": "USER", + "ACTION": "INFORM", + "STEP": "step1", + "SUMMARY": "Welcome", + "BODY": "", + } + + parsed_message = parse(message_str) + + for key in expected_output: + assert parsed_message[key].strip() == expected_output[key].strip() + + +def test_parse_message_with_whitespace(): + message_str = """ *** + FROM: CODER + TO: USER + ACTION: INFORM + STEP: step1 + SUMMARY: Welcome + BODY: + + Welcome to sirji-messages. Here's how you can start. + *** + """ + + expected_output = { + "FROM": "CODER", + "TO": "USER", + "ACTION": "INFORM", + "STEP": "step1", + "SUMMARY": "Welcome", + "BODY": "Welcome to sirji-messages. Here's how you can start.", + } + + parsed_message = parse(message_str) + + for key in expected_output: + assert parsed_message[key].strip() == expected_output[key].strip() \ No newline at end of file