mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
838a7fd61d
* 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
288 lines
11 KiB
Python
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"
|