Files
agent-framework/python/packages/ag-ui/tests/test_event_converters.py
T
Eduard van Valkenburg 838a7fd61d Python: [BREAKING] Types API Review improvements (#3647)
* Replace Role and FinishReason classes with NewType + Literal

- Remove EnumLike metaclass from _types.py
- Replace Role class with NewType('Role', str) + RoleLiteral
- Replace FinishReason class with NewType('FinishReason', str) + FinishReasonLiteral
- Update all usages across codebase to use string literals
- Remove .value access patterns (direct string comparison now works)
- Add backward compatibility for legacy dict serialization format
- Update tests to reflect new string-based types

Addresses #3591, #3615

* Simplify ChatResponse and AgentResponse type hints (#3592)

- Remove overloads from ChatResponse.__init__
- Remove text parameter from ChatResponse.__init__
- Remove | dict[str, Any] from finish_reason and usage_details params
- Remove **kwargs from AgentResponse.__init__
- Both now accept ChatMessage | Sequence[ChatMessage] | None for messages
- Update docstrings and examples to reflect changes
- Fix tests that were using removed kwargs
- Fix Role type hint usage in ag-ui utils

* Remove text parameter from ChatResponseUpdate and AgentResponseUpdate (#3597)

- Remove text parameter from ChatResponseUpdate.__init__
- Remove text parameter from AgentResponseUpdate.__init__
- Remove **kwargs from both update classes
- Simplify contents parameter type to Sequence[Content] | None
- Update all usages to use contents=[Content.from_text(...)] pattern
- Fix imports in test files
- Update docstrings and examples

* Rename from_chat_response_updates to from_updates (#3593)

- ChatResponse.from_chat_response_updates → ChatResponse.from_updates
- ChatResponse.from_chat_response_generator → ChatResponse.from_update_generator
- AgentResponse.from_agent_run_response_updates → AgentResponse.from_updates

* Remove try_parse_value method from ChatResponse and AgentResponse (#3595)

- Remove try_parse_value method from ChatResponse
- Remove try_parse_value method from AgentResponse
- Remove try_parse_value calls from from_updates and from_update_generator methods
- Update samples to use try/except with response.value instead
- Update tests to use response.value pattern
- Users should now use response.value with try/except for safe parsing

* Add agent_id to AgentResponse and clarify author_name documentation (#3596)

- Add agent_id parameter to AgentResponse class
- Document that author_name is on ChatMessage objects, not responses
- Update ChatResponse docstring with author_name note
- Update AgentResponse docstring with author_name note

* Simplify ChatMessage.__init__ signature (#3618)

- Make contents a positional argument accepting Sequence[Content | str]
- Auto-convert strings in contents to TextContent
- Remove overloads, keep text kwarg for backward compatibility with serialization
- Update _parse_content_list to handle string items
- Update all usages across codebase to use new format: ChatMessage("role", ["text"])

* Allow Content as input on run and get_response

- Update prepare_messages and normalize_messages to accept Content
- Update type signatures in _agents.py and _clients.py
- Add tests for Content input handling

* Fix ChatMessage usage across packages and samples

Update all remaining ChatMessage(role=..., text=...) to use new
ChatMessage('role', ['text']) signature.

* Fix Role string usage and response format parsing

- Fix redis provider: remove .value access on string literals
- Fix durabletask ensure_response_format: set _response_format before accessing .value

* Fix ollama .value and ai_model_id issues, handle None in content list

- Fix ollama _chat_client: remove .value on string literals
- Fix ollama _chat_client: rename ai_model_id to model_id
- Fix _parse_content_list: skip None values gracefully

* Fix A2AAgent type signature to include Content

* Fix Role/FinishReason NewType dict annotations and improve test coverage to 95%

* Fix mypy errors for Role/FinishReason NewType usage

* Fix Role.TOOL and Role.ASSISTANT usage in _orchestrator_helpers.py

* Fix Role NewType usage in durabletask _models.py
2026-02-04 10:13:23 +00:00

288 lines
11 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
"""Tests for AG-UI event converter."""
from agent_framework_ag_ui._event_converters import AGUIEventConverter
class TestAGUIEventConverter:
"""Test suite for AGUIEventConverter."""
def test_run_started_event(self) -> None:
"""Test conversion of RUN_STARTED event."""
converter = AGUIEventConverter()
event = {
"type": "RUN_STARTED",
"threadId": "thread_123",
"runId": "run_456",
}
update = converter.convert_event(event)
assert update is not None
assert update.role == "assistant"
assert update.additional_properties["thread_id"] == "thread_123"
assert update.additional_properties["run_id"] == "run_456"
assert converter.thread_id == "thread_123"
assert converter.run_id == "run_456"
def test_text_message_start_event(self) -> None:
"""Test conversion of TEXT_MESSAGE_START event."""
converter = AGUIEventConverter()
event = {
"type": "TEXT_MESSAGE_START",
"messageId": "msg_789",
}
update = converter.convert_event(event)
assert update is not None
assert update.role == "assistant"
assert update.message_id == "msg_789"
assert converter.current_message_id == "msg_789"
def test_text_message_content_event(self) -> None:
"""Test conversion of TEXT_MESSAGE_CONTENT event."""
converter = AGUIEventConverter()
event = {
"type": "TEXT_MESSAGE_CONTENT",
"messageId": "msg_1",
"delta": "Hello",
}
update = converter.convert_event(event)
assert update is not None
assert update.role == "assistant"
assert update.message_id == "msg_1"
assert len(update.contents) == 1
assert update.contents[0].text == "Hello"
def test_text_message_streaming(self) -> None:
"""Test streaming text across multiple TEXT_MESSAGE_CONTENT events."""
converter = AGUIEventConverter()
events = [
{"type": "TEXT_MESSAGE_CONTENT", "messageId": "msg_1", "delta": "Hello"},
{"type": "TEXT_MESSAGE_CONTENT", "messageId": "msg_1", "delta": " world"},
{"type": "TEXT_MESSAGE_CONTENT", "messageId": "msg_1", "delta": "!"},
]
updates = [converter.convert_event(event) for event in events]
assert all(update is not None for update in updates)
assert all(update.message_id == "msg_1" for update in updates)
assert updates[0].contents[0].text == "Hello"
assert updates[1].contents[0].text == " world"
assert updates[2].contents[0].text == "!"
def test_text_message_end_event(self) -> None:
"""Test conversion of TEXT_MESSAGE_END event."""
converter = AGUIEventConverter()
event = {
"type": "TEXT_MESSAGE_END",
"messageId": "msg_1",
}
update = converter.convert_event(event)
assert update is None
def test_tool_call_start_event(self) -> None:
"""Test conversion of TOOL_CALL_START event."""
converter = AGUIEventConverter()
event = {
"type": "TOOL_CALL_START",
"toolCallId": "call_123",
"toolName": "get_weather",
}
update = converter.convert_event(event)
assert update is not None
assert update.role == "assistant"
assert len(update.contents) == 1
assert update.contents[0].call_id == "call_123"
assert update.contents[0].name == "get_weather"
assert update.contents[0].arguments == ""
assert converter.current_tool_call_id == "call_123"
assert converter.current_tool_name == "get_weather"
def test_tool_call_start_with_tool_call_name(self) -> None:
"""Ensure TOOL_CALL_START with toolCallName still sets the tool name."""
converter = AGUIEventConverter()
event = {
"type": "TOOL_CALL_START",
"toolCallId": "call_abc",
"toolCallName": "get_weather",
}
update = converter.convert_event(event)
assert update is not None
assert update.contents[0].name == "get_weather"
assert converter.current_tool_name == "get_weather"
def test_tool_call_start_with_tool_call_name_snake_case(self) -> None:
"""Support tool_call_name snake_case field for backwards compatibility."""
converter = AGUIEventConverter()
event = {
"type": "TOOL_CALL_START",
"toolCallId": "call_snake",
"tool_call_name": "get_weather",
}
update = converter.convert_event(event)
assert update is not None
assert update.contents[0].name == "get_weather"
assert converter.current_tool_name == "get_weather"
def test_tool_call_args_streaming(self) -> None:
"""Test streaming tool arguments across multiple TOOL_CALL_ARGS events."""
converter = AGUIEventConverter()
converter.current_tool_call_id = "call_123"
converter.current_tool_name = "search"
events = [
{"type": "TOOL_CALL_ARGS", "delta": '{"query": "'},
{"type": "TOOL_CALL_ARGS", "delta": 'latest news"}'},
]
updates = [converter.convert_event(event) for event in events]
assert all(update is not None for update in updates)
assert updates[0].contents[0].arguments == '{"query": "'
assert updates[1].contents[0].arguments == 'latest news"}'
assert converter.accumulated_tool_args == '{"query": "latest news"}'
def test_tool_call_end_event(self) -> None:
"""Test conversion of TOOL_CALL_END event."""
converter = AGUIEventConverter()
converter.accumulated_tool_args = '{"location": "Seattle"}'
event = {
"type": "TOOL_CALL_END",
"toolCallId": "call_123",
}
update = converter.convert_event(event)
assert update is None
assert converter.accumulated_tool_args == ""
def test_tool_call_result_event(self) -> None:
"""Test conversion of TOOL_CALL_RESULT event."""
converter = AGUIEventConverter()
event = {
"type": "TOOL_CALL_RESULT",
"toolCallId": "call_123",
"result": {"temperature": 22, "condition": "sunny"},
}
update = converter.convert_event(event)
assert update is not None
assert update.role == "tool"
assert len(update.contents) == 1
assert update.contents[0].call_id == "call_123"
assert update.contents[0].result == {"temperature": 22, "condition": "sunny"}
def test_run_finished_event(self) -> None:
"""Test conversion of RUN_FINISHED event."""
converter = AGUIEventConverter()
converter.thread_id = "thread_123"
converter.run_id = "run_456"
event = {
"type": "RUN_FINISHED",
"threadId": "thread_123",
"runId": "run_456",
}
update = converter.convert_event(event)
assert update is not None
assert update.role == "assistant"
assert update.finish_reason == "stop"
assert update.additional_properties["thread_id"] == "thread_123"
assert update.additional_properties["run_id"] == "run_456"
def test_run_error_event(self) -> None:
"""Test conversion of RUN_ERROR event."""
converter = AGUIEventConverter()
converter.thread_id = "thread_123"
converter.run_id = "run_456"
event = {
"type": "RUN_ERROR",
"message": "Connection timeout",
}
update = converter.convert_event(event)
assert update is not None
assert update.role == "assistant"
assert update.finish_reason == "content_filter"
assert len(update.contents) == 1
assert update.contents[0].message == "Connection timeout"
assert update.contents[0].error_code == "RUN_ERROR"
def test_unknown_event_type(self) -> None:
"""Test handling of unknown event types."""
converter = AGUIEventConverter()
event = {
"type": "UNKNOWN_EVENT",
"data": "some data",
}
update = converter.convert_event(event)
assert update is None
def test_full_conversation_flow(self) -> None:
"""Test complete conversation flow with multiple event types."""
converter = AGUIEventConverter()
events = [
{"type": "RUN_STARTED", "threadId": "thread_1", "runId": "run_1"},
{"type": "TEXT_MESSAGE_START", "messageId": "msg_1"},
{"type": "TEXT_MESSAGE_CONTENT", "messageId": "msg_1", "delta": "I'll check"},
{"type": "TEXT_MESSAGE_CONTENT", "messageId": "msg_1", "delta": " the weather."},
{"type": "TEXT_MESSAGE_END", "messageId": "msg_1"},
{"type": "TOOL_CALL_START", "toolCallId": "call_1", "toolName": "get_weather"},
{"type": "TOOL_CALL_ARGS", "delta": '{"location": "Seattle"}'},
{"type": "TOOL_CALL_END", "toolCallId": "call_1"},
{"type": "TOOL_CALL_RESULT", "toolCallId": "call_1", "result": "Sunny, 72°F"},
{"type": "TEXT_MESSAGE_START", "messageId": "msg_2"},
{"type": "TEXT_MESSAGE_CONTENT", "messageId": "msg_2", "delta": "It's sunny!"},
{"type": "TEXT_MESSAGE_END", "messageId": "msg_2"},
{"type": "RUN_FINISHED", "threadId": "thread_1", "runId": "run_1"},
]
updates = [converter.convert_event(event) for event in events]
non_none_updates = [u for u in updates if u is not None]
assert len(non_none_updates) == 10
assert converter.thread_id == "thread_1"
assert converter.run_id == "run_1"
def test_multiple_tool_calls(self) -> None:
"""Test handling multiple tool calls in sequence."""
converter = AGUIEventConverter()
events = [
{"type": "TOOL_CALL_START", "toolCallId": "call_1", "toolName": "search"},
{"type": "TOOL_CALL_ARGS", "delta": '{"query": "weather"}'},
{"type": "TOOL_CALL_END", "toolCallId": "call_1"},
{"type": "TOOL_CALL_START", "toolCallId": "call_2", "toolName": "fetch"},
{"type": "TOOL_CALL_ARGS", "delta": '{"url": "http://api.weather.com"}'},
{"type": "TOOL_CALL_END", "toolCallId": "call_2"},
]
updates = [converter.convert_event(event) for event in events]
non_none_updates = [u for u in updates if u is not None]
assert len(non_none_updates) == 4
assert non_none_updates[0].contents[0].name == "search"
assert non_none_updates[2].contents[0].name == "fetch"