mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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
This commit is contained in:
committed by
GitHub
Unverified
parent
ef798629e5
commit
838a7fd61d
@@ -9,7 +9,6 @@ from collections.abc import Awaitable, Callable, Sequence
|
||||
from agent_framework import (
|
||||
ChatMessage,
|
||||
Content,
|
||||
Role,
|
||||
)
|
||||
from chatkit.types import (
|
||||
AssistantMessageItem,
|
||||
@@ -101,21 +100,21 @@ class ThreadItemConverter:
|
||||
|
||||
# If only text and no attachments, use text parameter for simplicity
|
||||
if text_content.strip() and not data_contents:
|
||||
user_message = ChatMessage(role=Role.USER, text=text_content.strip())
|
||||
user_message = ChatMessage("user", [text_content.strip()])
|
||||
else:
|
||||
# Build contents list with both text and attachments
|
||||
contents: list[Content] = []
|
||||
if text_content.strip():
|
||||
contents.append(Content.from_text(text=text_content.strip()))
|
||||
contents.extend(data_contents)
|
||||
user_message = ChatMessage(role=Role.USER, contents=contents)
|
||||
user_message = ChatMessage("user", contents)
|
||||
|
||||
# Handle quoted text if this is the last message
|
||||
messages = [user_message]
|
||||
if item.quoted_text and is_last_message:
|
||||
quoted_context = ChatMessage(
|
||||
role=Role.USER,
|
||||
text=f"The user is referring to this in particular:\n{item.quoted_text}",
|
||||
"user",
|
||||
[f"The user is referring to this in particular:\n{item.quoted_text}"],
|
||||
)
|
||||
# Prepend quoted context before the main message
|
||||
messages.insert(0, quoted_context)
|
||||
@@ -214,7 +213,7 @@ class ThreadItemConverter:
|
||||
message = converter.hidden_context_to_input(hidden_item)
|
||||
# Returns: ChatMessage(role=SYSTEM, text="<HIDDEN_CONTEXT>User's email: ...</HIDDEN_CONTEXT>")
|
||||
"""
|
||||
return ChatMessage(role=Role.SYSTEM, text=f"<HIDDEN_CONTEXT>{item.content}</HIDDEN_CONTEXT>")
|
||||
return ChatMessage("system", [f"<HIDDEN_CONTEXT>{item.content}</HIDDEN_CONTEXT>"])
|
||||
|
||||
def tag_to_message_content(self, tag: UserMessageTagContent) -> Content:
|
||||
"""Convert a ChatKit tag (@-mention) to Agent Framework content.
|
||||
@@ -293,7 +292,7 @@ class ThreadItemConverter:
|
||||
f"A message was displayed to the user that the following task was performed:\n<Task>\n{task_text}\n</Task>"
|
||||
)
|
||||
|
||||
return ChatMessage(role=Role.USER, text=text)
|
||||
return ChatMessage("user", [text])
|
||||
|
||||
def workflow_to_input(self, item: WorkflowItem) -> ChatMessage | list[ChatMessage] | None:
|
||||
"""Convert a ChatKit WorkflowItem to Agent Framework ChatMessage(s).
|
||||
@@ -348,7 +347,7 @@ class ThreadItemConverter:
|
||||
f"<Task>\n{task_text}\n</Task>"
|
||||
)
|
||||
|
||||
messages.append(ChatMessage(role=Role.USER, text=text))
|
||||
messages.append(ChatMessage("user", [text]))
|
||||
|
||||
return messages if messages else None
|
||||
|
||||
@@ -390,7 +389,7 @@ class ThreadItemConverter:
|
||||
try:
|
||||
widget_json = item.widget.model_dump_json(exclude_unset=True, exclude_none=True)
|
||||
text = f"The following graphical UI widget (id: {item.id}) was displayed to the user:{widget_json}"
|
||||
return ChatMessage(role=Role.USER, text=text)
|
||||
return ChatMessage("user", [text])
|
||||
except Exception:
|
||||
# If JSON serialization fails, skip the widget
|
||||
return None
|
||||
@@ -416,7 +415,7 @@ class ThreadItemConverter:
|
||||
if not text_parts:
|
||||
return None
|
||||
|
||||
return ChatMessage(role=Role.ASSISTANT, text="".join(text_parts))
|
||||
return ChatMessage("assistant", ["".join(text_parts)])
|
||||
|
||||
async def client_tool_call_to_input(self, item: ClientToolCallItem) -> ChatMessage | list[ChatMessage] | None:
|
||||
"""Convert a ChatKit ClientToolCallItem to Agent Framework ChatMessage(s).
|
||||
@@ -442,7 +441,7 @@ class ThreadItemConverter:
|
||||
|
||||
# Create function call message
|
||||
function_call_msg = ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
role="assistant",
|
||||
contents=[
|
||||
Content.from_function_call(
|
||||
call_id=item.call_id,
|
||||
@@ -454,7 +453,7 @@ class ThreadItemConverter:
|
||||
|
||||
# Create function result message
|
||||
function_result_msg = ChatMessage(
|
||||
role=Role.TOOL,
|
||||
role="tool",
|
||||
contents=[
|
||||
Content.from_function_result(
|
||||
call_id=item.call_id,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from agent_framework import ChatMessage, Role
|
||||
from agent_framework import ChatMessage
|
||||
from chatkit.types import UserMessageTextContent
|
||||
|
||||
from agent_framework_chatkit import ThreadItemConverter, simple_to_agent_input
|
||||
@@ -44,7 +44,7 @@ class TestThreadItemConverter:
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], ChatMessage)
|
||||
assert result[0].role == Role.USER
|
||||
assert result[0].role == "user"
|
||||
assert result[0].text == "Hello, how can you help me?"
|
||||
|
||||
async def test_to_agent_input_empty_text(self, converter):
|
||||
@@ -117,7 +117,7 @@ class TestThreadItemConverter:
|
||||
result = converter.hidden_context_to_input(hidden_item)
|
||||
|
||||
assert isinstance(result, ChatMessage)
|
||||
assert result.role == Role.SYSTEM
|
||||
assert result.role == "system"
|
||||
assert result.text == "<HIDDEN_CONTEXT>This is hidden context information</HIDDEN_CONTEXT>"
|
||||
|
||||
def test_tag_to_message_content(self, converter):
|
||||
@@ -234,7 +234,7 @@ class TestThreadItemConverter:
|
||||
|
||||
assert len(result) == 1
|
||||
message = result[0]
|
||||
assert message.role == Role.USER
|
||||
assert message.role == "user"
|
||||
assert len(message.contents) == 2
|
||||
|
||||
# First content should be text
|
||||
@@ -303,7 +303,7 @@ class TestThreadItemConverter:
|
||||
|
||||
result = converter.task_to_input(task_item)
|
||||
assert isinstance(result, ChatMessage)
|
||||
assert result.role == Role.USER
|
||||
assert result.role == "user"
|
||||
assert "Analysis: Analyzed the data" in result.text
|
||||
assert "<Task>" in result.text
|
||||
|
||||
@@ -385,7 +385,7 @@ class TestThreadItemConverter:
|
||||
|
||||
result = converter.widget_to_input(widget_item)
|
||||
assert isinstance(result, ChatMessage)
|
||||
assert result.role == Role.USER
|
||||
assert result.role == "user"
|
||||
assert "widget_1" in result.text
|
||||
assert "graphical UI widget" in result.text
|
||||
|
||||
@@ -418,5 +418,5 @@ class TestSimpleToAgentInput:
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], ChatMessage)
|
||||
assert result[0].role == Role.USER
|
||||
assert result[0].role == "user"
|
||||
assert result[0].text == "Test message"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
from agent_framework import AgentResponseUpdate, Content, Role
|
||||
from agent_framework import AgentResponseUpdate, Content
|
||||
from chatkit.types import (
|
||||
ThreadItemAddedEvent,
|
||||
ThreadItemDoneEvent,
|
||||
@@ -34,7 +34,7 @@ class TestStreamAgentResponse:
|
||||
"""Test streaming single text update."""
|
||||
|
||||
async def single_update_stream():
|
||||
yield AgentResponseUpdate(role=Role.ASSISTANT, contents=[Content.from_text(text="Hello world")])
|
||||
yield AgentResponseUpdate(role="assistant", contents=[Content.from_text(text="Hello world")])
|
||||
|
||||
events = []
|
||||
async for event in stream_agent_response(single_update_stream(), thread_id="test_thread"):
|
||||
@@ -59,8 +59,8 @@ class TestStreamAgentResponse:
|
||||
"""Test streaming multiple text updates."""
|
||||
|
||||
async def multiple_updates_stream():
|
||||
yield AgentResponseUpdate(role=Role.ASSISTANT, contents=[Content.from_text(text="Hello ")])
|
||||
yield AgentResponseUpdate(role=Role.ASSISTANT, contents=[Content.from_text(text="world!")])
|
||||
yield AgentResponseUpdate(role="assistant", contents=[Content.from_text(text="Hello ")])
|
||||
yield AgentResponseUpdate(role="assistant", contents=[Content.from_text(text="world!")])
|
||||
|
||||
events = []
|
||||
async for event in stream_agent_response(multiple_updates_stream(), thread_id="test_thread"):
|
||||
@@ -91,7 +91,7 @@ class TestStreamAgentResponse:
|
||||
return f"custom_{item_type}_123"
|
||||
|
||||
async def single_update_stream():
|
||||
yield AgentResponseUpdate(role=Role.ASSISTANT, contents=[Content.from_text(text="Test")])
|
||||
yield AgentResponseUpdate(role="assistant", contents=[Content.from_text(text="Test")])
|
||||
|
||||
events = []
|
||||
async for event in stream_agent_response(
|
||||
@@ -107,8 +107,8 @@ class TestStreamAgentResponse:
|
||||
"""Test streaming updates with empty content."""
|
||||
|
||||
async def empty_content_stream():
|
||||
yield AgentResponseUpdate(role=Role.ASSISTANT, contents=[])
|
||||
yield AgentResponseUpdate(role=Role.ASSISTANT, contents=None)
|
||||
yield AgentResponseUpdate(role="assistant", contents=[])
|
||||
yield AgentResponseUpdate(role="assistant", contents=None)
|
||||
|
||||
events = []
|
||||
async for event in stream_agent_response(empty_content_stream(), thread_id="test_thread"):
|
||||
@@ -131,7 +131,7 @@ class TestStreamAgentResponse:
|
||||
non_text_content.text = None
|
||||
|
||||
async def non_text_stream():
|
||||
yield AgentResponseUpdate(role=Role.ASSISTANT, contents=[non_text_content])
|
||||
yield AgentResponseUpdate(role="assistant", contents=[non_text_content])
|
||||
|
||||
events = []
|
||||
async for event in stream_agent_response(non_text_stream(), thread_id="test_thread"):
|
||||
|
||||
Reference in New Issue
Block a user