diff --git a/autogen/agentchat/chat.py b/autogen/agentchat/chat.py index b02de9a19de..62a3fd358de 100644 --- a/autogen/agentchat/chat.py +++ b/autogen/agentchat/chat.py @@ -14,11 +14,10 @@ from functools import partial from typing import Any, TypedDict -from ..code_utils import content_str from ..doc_utils import export_module from ..events.agent_events import PostCarryoverProcessingEvent from ..io.base import IOStream -from .utils import consolidate_chat_info +from .utils import consolidate_chat_info, normalize_content logger = logging.getLogger(__name__) Prerequisite = tuple[int, int] @@ -134,9 +133,7 @@ def _post_process_carryover_item(carryover_item): return carryover_item elif isinstance(carryover_item, dict) and "content" in carryover_item: content_value = carryover_item.get("content") - if isinstance(content_value, (str, list)) or content_value is None: - return content_str(content_value) - return str(content_value) + return normalize_content(content_value) else: return str(carryover_item) diff --git a/autogen/agentchat/conversable_agent.py b/autogen/agentchat/conversable_agent.py index f4db4944b72..a6a2d85e947 100644 --- a/autogen/agentchat/conversable_agent.py +++ b/autogen/agentchat/conversable_agent.py @@ -79,7 +79,7 @@ from .group.context_variables import ContextVariables from .group.guardrails import Guardrail from .group.handoffs import Handoffs -from .utils import consolidate_chat_info, gather_usage_summary +from .utils import consolidate_chat_info, gather_usage_summary, normalize_message_to_dict if TYPE_CHECKING: from .group.on_condition import OnCondition @@ -1016,12 +1016,7 @@ def _message_to_dict(message: dict[str, Any] | str) -> dict: The message can be a string or a dictionary. The string will be put in the "content" field of the new dictionary. """ - if isinstance(message, str): - return {"content": message} - elif isinstance(message, dict): - return message - else: - return dict(message) + return normalize_message_to_dict(message) @staticmethod def _normalize_name(name): diff --git a/autogen/agentchat/group/group_tool_executor.py b/autogen/agentchat/group/group_tool_executor.py index 4c7f70974ab..350269dd253 100644 --- a/autogen/agentchat/group/group_tool_executor.py +++ b/autogen/agentchat/group/group_tool_executor.py @@ -7,12 +7,12 @@ from copy import deepcopy from typing import Annotated, Any -from ...code_utils import content_str from ...oai import OpenAIWrapper from ...tools import Depends, Tool from ...tools.dependency_injection import inject_params, on from ..agent import Agent from ..conversable_agent import ConversableAgent +from ..utils import normalize_content from .context_variables import __CONTEXT_VARIABLES_PARAM_NAME__, ContextVariables from .reply_result import ReplyResult from .targets.transition_target import TransitionTarget @@ -205,9 +205,7 @@ def _generate_group_tool_reply( next_target = content # Serialize the content to a string - normalized_content = ( - content_str(content) if isinstance(content, (str, list)) or content is None else str(content) - ) + normalized_content = normalize_content(content) tool_response["content"] = normalized_content tool_responses_inner.append(tool_response) diff --git a/autogen/agentchat/group/safeguards/enforcer.py b/autogen/agentchat/group/safeguards/enforcer.py index c1b47f24292..5a13b8d32ef 100644 --- a/autogen/agentchat/group/safeguards/enforcer.py +++ b/autogen/agentchat/group/safeguards/enforcer.py @@ -9,11 +9,11 @@ from collections.abc import Callable from typing import Any -from ....code_utils import content_str from ....io.base import IOStream from ....llm_config import LLMConfig from ...conversable_agent import ConversableAgent from ...groupchat import GroupChatManager +from ...utils import normalize_content from ..guardrails import LLMGuardrail, RegexGuardrail from ..targets.transition_target import TransitionTarget from .events import SafeguardEvent @@ -24,12 +24,10 @@ class SafeguardEnforcer: @staticmethod def _stringify_content(value: Any) -> str: - if isinstance(value, (str, list)) or value is None: - try: - return content_str(value) - except (TypeError, ValueError, AssertionError): - pass - return "" if value is None else str(value) + try: + return normalize_content(value) + except (TypeError, ValueError, AssertionError): + return "" if value is None else str(value) def __init__( self, diff --git a/autogen/agentchat/utils.py b/autogen/agentchat/utils.py index afeee81db96..5e0b7b7d803 100644 --- a/autogen/agentchat/utils.py +++ b/autogen/agentchat/utils.py @@ -7,10 +7,67 @@ import re from typing import Any +from ..code_utils import content_str from ..doc_utils import export_module from .agent import Agent +def normalize_content(content: Any) -> str: + """Normalize content to a string format. + + This function handles various content types commonly used in agent messages: + - str, list, or None: Uses content_str() for proper formatting + - Any other type: Converts to string using str() + + Args: + content: The content to normalize (str, list, dict, or any other type) + + Returns: + str: The normalized content as a string + + Example: + >>> normalize_content("Hello") + 'Hello' + >>> normalize_content(["text", {"type": "text", "text": "world"}]) + 'text world' + >>> normalize_content(None) + '' + >>> normalize_content({"key": "value"}) + "{'key': 'value'}" + """ + if isinstance(content, (str, list)) or content is None: + return content_str(content) + return str(content) + + +def normalize_message_to_dict(message: dict[str, Any] | str) -> dict[str, Any]: + """Convert a message to a dictionary format. + + This function normalizes messages that can be either strings or dictionaries: + - If str: Wraps in a dict with "content" field + - If dict: Returns as-is + - Otherwise: Converts to dict using dict() constructor + + Args: + message: The message to normalize (str, dict, or dict-like object) + + Returns: + dict[str, Any]: The message in dictionary format + + Example: + >>> normalize_message_to_dict("Hello") + {'content': 'Hello'} + >>> normalize_message_to_dict({"role": "user", "content": "Hi"}) + {'role': 'user', 'content': 'Hi'} + """ + if isinstance(message, str): + return {"content": message} + elif isinstance(message, dict): + return message + else: + return dict(message) + + def consolidate_chat_info( chat_info: dict[str, Any] | list[dict[str, Any]], uniform_sender: Agent | None = None ) -> None: diff --git a/test/agentchat/test_agentchat_utils.py b/test/agentchat/test_agentchat_utils.py index 42c06e57a44..08b4548d156 100644 --- a/test/agentchat/test_agentchat_utils.py +++ b/test/agentchat/test_agentchat_utils.py @@ -79,5 +79,175 @@ def test_tag_parsing(test_case: dict[str, str | list[dict[str, str | dict[str, s assert result == expected +def test_normalize_content_with_string(): + """Test normalize_content with string input.""" + from autogen.agentchat.utils import normalize_content + + assert normalize_content("Hello world") == "Hello world" + assert normalize_content("") == "" + assert normalize_content("Multi\nline\nstring") == "Multi\nline\nstring" + + +def test_normalize_content_with_list(): + """Test normalize_content with list input (multimodal content).""" + from autogen.agentchat.utils import normalize_content + + # Simple text content + content = [{"type": "text", "text": "Hello"}] + result = normalize_content(content) + assert result == "Hello" + + # Multiple content items + content = [{"type": "text", "text": "Hello"}, {"type": "text", "text": "World"}] + result = normalize_content(content) + assert "Hello" in result and "World" in result + + # Empty list + assert normalize_content([]) == "" + + +def test_normalize_content_with_none(): + """Test normalize_content with None input.""" + from autogen.agentchat.utils import normalize_content + + assert normalize_content(None) == "" + + +def test_normalize_content_with_dict(): + """Test normalize_content with dict input (converts to string).""" + from autogen.agentchat.utils import normalize_content + + content = {"key": "value", "number": 42} + result = normalize_content(content) + assert isinstance(result, str) + assert "key" in result or "value" in result + + +def test_normalize_content_with_other_types(): + """Test normalize_content with other types (int, float, bool, etc.).""" + from autogen.agentchat.utils import normalize_content + + assert normalize_content(42) == "42" + assert normalize_content(3.14) == "3.14" + assert normalize_content(True) == "True" + assert normalize_content(False) == "False" + + +def test_normalize_message_to_dict_with_string(): + """Test normalize_message_to_dict with string input.""" + from autogen.agentchat.utils import normalize_message_to_dict + + result = normalize_message_to_dict("Hello world") + assert result == {"content": "Hello world"} + + result = normalize_message_to_dict("") + assert result == {"content": ""} + + +def test_normalize_message_to_dict_with_dict(): + """Test normalize_message_to_dict with dict input.""" + from autogen.agentchat.utils import normalize_message_to_dict + + # Simple message dict + message = {"role": "user", "content": "Hello"} + result = normalize_message_to_dict(message) + assert result == message + assert result is message # Should return same object + + # Complex message with multiple fields + message = { + "role": "assistant", + "content": "Response", + "name": "agent1", + "function_call": {"name": "func", "arguments": "{}"}, + } + result = normalize_message_to_dict(message) + assert result == message + + +def test_normalize_message_to_dict_with_dict_like(): + """Test normalize_message_to_dict with dict-like objects.""" + from autogen.agentchat.utils import normalize_message_to_dict + + # Create a dict-like object (has items() method) + class DictLike: + def __init__(self, data): + self.data = data + + def items(self): + return self.data.items() + + def keys(self): + return self.data.keys() + + def values(self): + return self.data.values() + + def __getitem__(self, key): + return self.data[key] + + dict_like = DictLike({"role": "user", "content": "Test"}) + result = normalize_message_to_dict(dict_like) + assert isinstance(result, dict) + assert result["role"] == "user" + assert result["content"] == "Test" + + +def test_normalize_content_edge_cases(): + """Test normalize_content with edge cases.""" + import pytest + + from autogen.agentchat.utils import normalize_content + + # Nested lists - should raise TypeError from content_str + nested = [{"type": "text", "text": "Level 1"}, [{"type": "text", "text": "Level 2"}]] + with pytest.raises(TypeError, match="Wrong content format"): + normalize_content(nested) + + # List with non-dict items - should raise TypeError from content_str + mixed = [{"type": "text", "text": "Text"}, "plain string", 42] + with pytest.raises(TypeError, match="Wrong content format"): + normalize_content(mixed) + + # List with image content + image_content = [ + {"type": "text", "text": "Check this image:"}, + {"type": "image_url", "image_url": {"url": "http://example.com/image.png"}}, + ] + result = normalize_content(image_content) + assert isinstance(result, str) + assert "Check this image:" in result + + +def test_normalize_message_to_dict_preserves_original(): + """Test that normalize_message_to_dict doesn't modify original dict.""" + from autogen.agentchat.utils import normalize_message_to_dict + + original = {"role": "user", "content": "Original"} + result = normalize_message_to_dict(original) + + # Modify result + result["content"] = "Modified" + + # Original should also be modified since it's the same object + assert original["content"] == "Modified" + + # For string input, original is not affected + original_str = "Original string" + result = normalize_message_to_dict(original_str) + result["content"] = "Modified" + assert original_str == "Original string" + + if __name__ == "__main__": test_tag_parsing(TAG_PARSING_TESTS[0]) + test_normalize_content_with_string() + test_normalize_content_with_list() + test_normalize_content_with_none() + test_normalize_content_with_dict() + test_normalize_content_with_other_types() + test_normalize_message_to_dict_with_string() + test_normalize_message_to_dict_with_dict() + test_normalize_message_to_dict_with_dict_like() + test_normalize_content_edge_cases() + test_normalize_message_to_dict_preserves_original()