Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions autogen/agentchat/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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)

Expand Down
9 changes: 2 additions & 7 deletions autogen/agentchat/conversable_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
6 changes: 2 additions & 4 deletions autogen/agentchat/group/group_tool_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
12 changes: 5 additions & 7 deletions autogen/agentchat/group/safeguards/enforcer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
57 changes: 57 additions & 0 deletions autogen/agentchat/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
170 changes: 170 additions & 0 deletions test/agentchat/test_agentchat_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you import function in each test? Can we import it on top of the file?


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__":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, remove manual-run block from the test file

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()
Loading