You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

test_messages.py 14 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. import json
  2. import uuid
  3. from datetime import datetime, timezone
  4. from typing import List
  5. import pytest
  6. from autogen_agentchat.messages import (
  7. AgentEvent,
  8. ChatMessage,
  9. HandoffMessage,
  10. MessageFactory,
  11. ModelClientStreamingChunkEvent,
  12. MultiModalMessage,
  13. StopMessage,
  14. StructuredMessage,
  15. StructuredMessageFactory,
  16. TextMessage,
  17. ToolCallExecutionEvent,
  18. ToolCallRequestEvent,
  19. )
  20. from autogen_core import FunctionCall
  21. from autogen_core.models import FunctionExecutionResult
  22. from pydantic import BaseModel
  23. class TestContent(BaseModel):
  24. """Test content model."""
  25. field1: str
  26. field2: int
  27. def test_structured_message() -> None:
  28. # Create a structured message with the test content
  29. message = StructuredMessage[TestContent](
  30. source="test_agent",
  31. content=TestContent(field1="test", field2=42),
  32. )
  33. # Check that the message type is correct
  34. assert message.type == "StructuredMessage[TestContent]" # type: ignore[comparison-overlap]
  35. # Check that the content is of the correct type
  36. assert isinstance(message.content, TestContent)
  37. # Check that the content fields are set correctly
  38. assert message.content.field1 == "test"
  39. assert message.content.field2 == 42
  40. # Check that model_dump works correctly
  41. dumped_message = message.model_dump()
  42. assert dumped_message["source"] == "test_agent"
  43. assert dumped_message["content"]["field1"] == "test"
  44. assert dumped_message["content"]["field2"] == 42
  45. assert dumped_message["type"] == "StructuredMessage[TestContent]"
  46. def test_structured_message_component() -> None:
  47. # Create a structured message with the test content
  48. format_string = "this is a string {field1} and this is an int {field2}"
  49. s_m = StructuredMessageFactory(input_model=TestContent, format_string=format_string)
  50. config = s_m.dump_component()
  51. s_m_dyn = StructuredMessageFactory.load_component(config)
  52. message = s_m_dyn.StructuredMessage(
  53. source="test_agent", content=s_m_dyn.ContentModel(field1="test", field2=42), format_string=s_m_dyn.format_string
  54. )
  55. assert isinstance(message.content, s_m_dyn.ContentModel)
  56. assert not isinstance(message.content, TestContent)
  57. assert message.content.field1 == "test" # type: ignore[attr-defined]
  58. assert message.content.field2 == 42 # type: ignore[attr-defined]
  59. dumped_message = message.model_dump()
  60. assert dumped_message["source"] == "test_agent"
  61. assert dumped_message["content"]["field1"] == "test"
  62. assert dumped_message["content"]["field2"] == 42
  63. assert message.to_model_text() == format_string.format(field1="test", field2=42)
  64. def test_message_factory() -> None:
  65. factory = MessageFactory()
  66. # Text message data
  67. text_data = {
  68. "type": "TextMessage",
  69. "source": "test_agent",
  70. "content": "Hello, world!",
  71. }
  72. # Create a TextMessage instance
  73. text_message = factory.create(text_data)
  74. assert isinstance(text_message, TextMessage)
  75. assert text_message.source == "test_agent"
  76. assert text_message.content == "Hello, world!"
  77. assert text_message.type == "TextMessage" # type: ignore[comparison-overlap]
  78. # Handoff message data
  79. handoff_data = {
  80. "type": "HandoffMessage",
  81. "source": "test_agent",
  82. "content": "handoff to another agent",
  83. "target": "target_agent",
  84. }
  85. # Create a HandoffMessage instance
  86. handoff_message = factory.create(handoff_data)
  87. assert isinstance(handoff_message, HandoffMessage)
  88. assert handoff_message.source == "test_agent"
  89. assert handoff_message.content == "handoff to another agent"
  90. assert handoff_message.target == "target_agent"
  91. assert handoff_message.type == "HandoffMessage" # type: ignore[comparison-overlap]
  92. # Structured message data
  93. structured_data = {
  94. "type": "StructuredMessage[TestContent]",
  95. "source": "test_agent",
  96. "content": {
  97. "field1": "test",
  98. "field2": 42,
  99. },
  100. }
  101. # Create a StructuredMessage instance -- this will fail because the type
  102. # is not registered in the factory.
  103. with pytest.raises(ValueError):
  104. structured_message = factory.create(structured_data)
  105. # Register the StructuredMessage type in the factory
  106. factory.register(StructuredMessage[TestContent])
  107. # Create a StructuredMessage instance
  108. structured_message = factory.create(structured_data)
  109. assert isinstance(structured_message, StructuredMessage)
  110. assert isinstance(structured_message.content, TestContent) # type: ignore[reportUnkownMemberType]
  111. assert structured_message.source == "test_agent"
  112. assert structured_message.content.field1 == "test"
  113. assert structured_message.content.field2 == 42
  114. assert structured_message.type == "StructuredMessage[TestContent]" # type: ignore[comparison-overlap]
  115. sm_factory = StructuredMessageFactory(input_model=TestContent, format_string=None, content_model_name="TestContent")
  116. config = sm_factory.dump_component()
  117. config.config["content_model_name"] = "DynamicTestContent"
  118. sm_factory_dynamic = StructuredMessageFactory.load_component(config)
  119. factory.register(sm_factory_dynamic.StructuredMessage)
  120. msg = sm_factory_dynamic.StructuredMessage(
  121. content=sm_factory_dynamic.ContentModel(field1="static", field2=123), source="static_agent"
  122. )
  123. restored = factory.create(msg.dump())
  124. assert isinstance(restored, StructuredMessage)
  125. assert isinstance(restored.content, sm_factory_dynamic.ContentModel) # type: ignore[reportUnkownMemberType]
  126. assert restored.source == "static_agent"
  127. assert restored.content.field1 == "static" # type: ignore[attr-defined]
  128. assert restored.content.field2 == 123 # type: ignore[attr-defined]
  129. class TestContainer(BaseModel):
  130. chat_messages: List[ChatMessage]
  131. agent_events: List[AgentEvent]
  132. def test_union_types() -> None:
  133. # Create a few messages.
  134. chat_messages: List[ChatMessage] = [
  135. TextMessage(source="user", content="Hello!"),
  136. MultiModalMessage(source="user", content=["Hello!", "World!"]),
  137. HandoffMessage(source="user", content="handoff to another agent", target="target_agent"),
  138. StopMessage(source="user", content="stop"),
  139. ]
  140. # Create a few agent events.
  141. agent_events: List[AgentEvent] = [
  142. ModelClientStreamingChunkEvent(source="user", content="Hello!"),
  143. ToolCallRequestEvent(
  144. content=[
  145. FunctionCall(id="1", name="test_function", arguments=json.dumps({"arg1": "value1", "arg2": "value2"}))
  146. ],
  147. source="user",
  148. ),
  149. ToolCallExecutionEvent(
  150. content=[FunctionExecutionResult(call_id="1", content="result", name="test")], source="user"
  151. ),
  152. ]
  153. # Create a container with the messages.
  154. container = TestContainer(chat_messages=chat_messages, agent_events=agent_events)
  155. # Dump the container to JSON.
  156. data = container.model_dump()
  157. # Load the container from JSON.
  158. loaded_container = TestContainer.model_validate(data)
  159. assert loaded_container.chat_messages == chat_messages
  160. assert loaded_container.agent_events == agent_events
  161. def test_message_id_field() -> None:
  162. """Test that messages have unique ID fields automatically generated."""
  163. # Test BaseChatMessage subclass (TextMessage)
  164. message1 = TextMessage(source="test_agent", content="Hello, world!")
  165. message2 = TextMessage(source="test_agent", content="Hello, world!")
  166. # Check that IDs are present and unique
  167. assert hasattr(message1, "id")
  168. assert hasattr(message2, "id")
  169. assert message1.id != message2.id
  170. assert isinstance(message1.id, str)
  171. assert isinstance(message2.id, str)
  172. # Check that IDs are valid UUIDs
  173. try:
  174. uuid.UUID(message1.id)
  175. uuid.UUID(message2.id)
  176. except ValueError:
  177. pytest.fail("Generated IDs are not valid UUIDs")
  178. # Test BaseAgentEvent subclass (ModelClientStreamingChunkEvent)
  179. event1 = ModelClientStreamingChunkEvent(source="test_agent", content="chunk1")
  180. event2 = ModelClientStreamingChunkEvent(source="test_agent", content="chunk2")
  181. # Check that IDs are present and unique
  182. assert hasattr(event1, "id")
  183. assert hasattr(event2, "id")
  184. assert event1.id != event2.id
  185. assert isinstance(event1.id, str)
  186. assert isinstance(event2.id, str)
  187. # Check that IDs are valid UUIDs
  188. try:
  189. uuid.UUID(event1.id)
  190. uuid.UUID(event2.id)
  191. except ValueError:
  192. pytest.fail("Generated IDs are not valid UUIDs")
  193. def test_custom_message_id() -> None:
  194. """Test that custom IDs can be provided."""
  195. custom_id = "custom-message-id-123"
  196. message = TextMessage(id=custom_id, source="test_agent", content="Hello, world!")
  197. assert message.id == custom_id
  198. custom_event_id = "custom-event-id-456"
  199. event = ModelClientStreamingChunkEvent(id=custom_event_id, source="test_agent", content="chunk")
  200. assert event.id == custom_event_id
  201. def test_streaming_chunk_full_message_id() -> None:
  202. """Test the full_message_id field in ModelClientStreamingChunkEvent."""
  203. # Test without full_message_id
  204. chunk1 = ModelClientStreamingChunkEvent(source="test_agent", content="chunk1")
  205. assert chunk1.full_message_id is None
  206. # Test with full_message_id
  207. full_msg_id = "full-message-123"
  208. chunk2 = ModelClientStreamingChunkEvent(source="test_agent", content="chunk2", full_message_id=full_msg_id)
  209. assert chunk2.full_message_id == full_msg_id
  210. # Test that chunk has its own ID separate from full_message_id
  211. assert chunk2.id != chunk2.full_message_id
  212. assert isinstance(chunk2.id, str)
  213. # Verify chunk ID is a valid UUID
  214. try:
  215. uuid.UUID(chunk2.id)
  216. except ValueError:
  217. pytest.fail("Chunk ID is not a valid UUID")
  218. def test_message_serialization_with_id() -> None:
  219. """Test that messages with IDs serialize and deserialize correctly."""
  220. # Create a message with auto-generated ID
  221. original_message = TextMessage(source="test_agent", content="Hello, world!")
  222. original_id = original_message.id
  223. # Serialize to dict
  224. message_data = original_message.model_dump()
  225. assert "id" in message_data
  226. assert message_data["id"] == original_id
  227. # Deserialize from dict
  228. restored_message = TextMessage.model_validate(message_data)
  229. assert restored_message.id == original_id
  230. assert restored_message.source == "test_agent"
  231. assert restored_message.content == "Hello, world!"
  232. # Test with streaming chunk event
  233. original_chunk = ModelClientStreamingChunkEvent(
  234. source="test_agent", content="chunk", full_message_id="full-msg-123"
  235. )
  236. original_chunk_id = original_chunk.id
  237. # Serialize to dict
  238. chunk_data = original_chunk.model_dump()
  239. assert "id" in chunk_data
  240. assert "full_message_id" in chunk_data
  241. assert chunk_data["id"] == original_chunk_id
  242. assert chunk_data["full_message_id"] == "full-msg-123"
  243. # Deserialize from dict
  244. restored_chunk = ModelClientStreamingChunkEvent.model_validate(chunk_data)
  245. assert restored_chunk.id == original_chunk_id
  246. assert restored_chunk.full_message_id == "full-msg-123"
  247. assert restored_chunk.content == "chunk"
  248. def test_datetime_serialization_in_messages() -> None:
  249. """Test that datetime objects in messages are properly serialized to JSON-compatible format.
  250. This test validates the fix for issue #6793 where datetime objects in message
  251. created_at fields caused JSON serialization errors when saving team state.
  252. """
  253. # Create a specific datetime for testing
  254. test_datetime = datetime(2023, 12, 25, 10, 30, 45, 123456, timezone.utc)
  255. # Test BaseChatMessage subclass with datetime
  256. chat_message = TextMessage(source="test_agent", content="Hello, world!", created_at=test_datetime)
  257. # Test that dump() returns JSON-serializable data
  258. chat_message_data = chat_message.dump()
  259. # Verify that the datetime is converted to a string in ISO format
  260. assert isinstance(chat_message_data["created_at"], str)
  261. # Pydantic JSON mode converts UTC timezone to 'Z' format instead of '+00:00'
  262. expected_iso = test_datetime.isoformat().replace("+00:00", "Z")
  263. assert chat_message_data["created_at"] == expected_iso
  264. # Verify that the dumped data is JSON serializable
  265. json_string = json.dumps(chat_message_data)
  266. assert isinstance(json_string, str)
  267. # Test round-trip serialization (dump -> load)
  268. restored_chat_message = TextMessage.load(chat_message_data)
  269. assert restored_chat_message.source == "test_agent"
  270. assert restored_chat_message.content == "Hello, world!"
  271. assert restored_chat_message.created_at == test_datetime
  272. # Test BaseAgentEvent subclass with datetime
  273. agent_event = ModelClientStreamingChunkEvent(source="test_agent", content="chunk", created_at=test_datetime)
  274. # Test that dump() returns JSON-serializable data
  275. agent_event_data = agent_event.dump()
  276. # Verify that the datetime is converted to a string in ISO format
  277. assert isinstance(agent_event_data["created_at"], str)
  278. assert agent_event_data["created_at"] == expected_iso
  279. # Verify that the dumped data is JSON serializable
  280. json_string = json.dumps(agent_event_data)
  281. assert isinstance(json_string, str)
  282. # Test round-trip serialization (dump -> load)
  283. restored_agent_event = ModelClientStreamingChunkEvent.load(agent_event_data)
  284. assert restored_agent_event.source == "test_agent"
  285. assert restored_agent_event.content == "chunk"
  286. assert restored_agent_event.created_at == test_datetime
  287. # Test with auto-generated datetime (default created_at)
  288. auto_message = TextMessage(source="test_agent", content="Auto datetime test")
  289. auto_message_data = auto_message.dump()
  290. # Verify datetime is serialized as string
  291. assert isinstance(auto_message_data["created_at"], str)
  292. # Verify JSON serialization works without errors
  293. json.dumps(auto_message_data)
  294. # Test round-trip with auto-generated datetime
  295. restored_auto_message = TextMessage.load(auto_message_data)
  296. assert restored_auto_message.created_at == auto_message.created_at