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:
Eduard van Valkenburg
2026-02-04 10:13:23 +00:00
committed by GitHub
parent ef798629e5
commit 838a7fd61d
341 changed files with 3766 additions and 3228 deletions
@@ -334,7 +334,7 @@ class AGUIChatClient(BaseChatClient[TAGUIChatOptions], Generic[TAGUIChatOptions]
Returns:
ChatResponse object
"""
return await ChatResponse.from_chat_response_generator(
return await ChatResponse.from_update_generator(
self._inner_get_streaming_response(
messages=messages,
options=options,
@@ -7,8 +7,6 @@ from typing import Any
from agent_framework import (
ChatResponseUpdate,
Content,
FinishReason,
Role,
)
@@ -86,7 +84,7 @@ class AGUIEventConverter:
self.run_id = event.get("runId")
return ChatResponseUpdate(
role=Role.ASSISTANT,
role="assistant",
contents=[],
additional_properties={
"thread_id": self.thread_id,
@@ -98,7 +96,7 @@ class AGUIEventConverter:
"""Handle TEXT_MESSAGE_START event."""
self.current_message_id = event.get("messageId")
return ChatResponseUpdate(
role=Role.ASSISTANT,
role="assistant",
message_id=self.current_message_id,
contents=[],
)
@@ -112,7 +110,7 @@ class AGUIEventConverter:
self.current_message_id = message_id
return ChatResponseUpdate(
role=Role.ASSISTANT,
role="assistant",
message_id=self.current_message_id,
contents=[Content.from_text(text=delta)],
)
@@ -128,7 +126,7 @@ class AGUIEventConverter:
self.accumulated_tool_args = ""
return ChatResponseUpdate(
role=Role.ASSISTANT,
role="assistant",
contents=[
Content.from_function_call(
call_id=self.current_tool_call_id or "",
@@ -144,7 +142,7 @@ class AGUIEventConverter:
self.accumulated_tool_args += delta
return ChatResponseUpdate(
role=Role.ASSISTANT,
role="assistant",
contents=[
Content.from_function_call(
call_id=self.current_tool_call_id or "",
@@ -165,7 +163,7 @@ class AGUIEventConverter:
result = event.get("result") if event.get("result") is not None else event.get("content")
return ChatResponseUpdate(
role=Role.TOOL,
role="tool",
contents=[
Content.from_function_result(
call_id=tool_call_id,
@@ -177,8 +175,8 @@ class AGUIEventConverter:
def _handle_run_finished(self, event: dict[str, Any]) -> ChatResponseUpdate:
"""Handle RUN_FINISHED event."""
return ChatResponseUpdate(
role=Role.ASSISTANT,
finish_reason=FinishReason.STOP,
role="assistant",
finish_reason="stop",
contents=[],
additional_properties={
"thread_id": self.thread_id,
@@ -191,8 +189,8 @@ class AGUIEventConverter:
error_message = event.get("message", "Unknown error")
return ChatResponseUpdate(
role=Role.ASSISTANT,
finish_reason=FinishReason.CONTENT_FILTER,
role="assistant",
finish_reason="content_filter",
contents=[
Content.from_error(
message=error_message,
@@ -9,7 +9,6 @@ from typing import Any, cast
from agent_framework import (
ChatMessage,
Content,
Role,
prepare_function_call_results,
)
@@ -269,7 +268,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha
def _find_matching_func_call(call_id: str) -> Content | None:
for prev_msg in result:
role_val = prev_msg.role.value if hasattr(prev_msg.role, "value") else str(prev_msg.role)
role_val = prev_msg.role if hasattr(prev_msg.role, "value") else str(prev_msg.role)
if role_val != "assistant":
continue
for content in prev_msg.contents or []:
@@ -287,7 +286,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha
return str(explicit_call_id)
for prev_msg in result:
role_val = prev_msg.role.value if hasattr(prev_msg.role, "value") else str(prev_msg.role)
role_val = prev_msg.role if hasattr(prev_msg.role, "value") else str(prev_msg.role)
if role_val != "assistant":
continue
direct_call = None
@@ -396,7 +395,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha
m
for m in result
if not (
(m.role.value if hasattr(m.role, "value") else str(m.role)) == "tool"
(m.role if hasattr(m.role, "value") else str(m.role)) == "tool"
and any(
c.type == "function_result" and c.call_id == approval_call_id
for c in (m.contents or [])
@@ -473,14 +472,14 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha
additional_properties={"ag_ui_state_args": state_args} if state_args else None,
)
chat_msg = ChatMessage(
role=Role.USER,
role="user",
contents=[approval_response],
)
else:
# No matching function call found - this is likely a confirm_changes approval
# Keep the old behavior for backwards compatibility
chat_msg = ChatMessage(
role=Role.USER,
role="user",
contents=[Content.from_text(text=approval_payload_text)],
additional_properties={"is_tool_result": True, "tool_call_id": str(tool_call_id or "")},
)
@@ -500,7 +499,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha
else:
func_result = str(result_content)
chat_msg = ChatMessage(
role=Role.TOOL,
role="tool",
contents=[Content.from_function_result(call_id=str(tool_call_id), result=func_result)],
)
if "id" in msg:
@@ -516,7 +515,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha
result_content = msg.get("result", msg.get("content", ""))
chat_msg = ChatMessage(
role=Role.TOOL,
role="tool",
contents=[Content.from_function_result(call_id=str(tool_call_id), result=result_content)],
)
if "id" in msg:
@@ -554,7 +553,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha
arguments=arguments,
)
)
chat_msg = ChatMessage(role=Role.ASSISTANT, contents=contents)
chat_msg = ChatMessage("assistant", contents)
if "id" in msg:
chat_msg.message_id = msg["id"]
result.append(chat_msg)
@@ -562,7 +561,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha
# No special handling required for assistant/plain messages here
role = AGUI_TO_FRAMEWORK_ROLE.get(role_str, Role.USER)
role = AGUI_TO_FRAMEWORK_ROLE.get(role_str, "user")
# Check if this message contains function approvals
if "function_approvals" in msg and msg["function_approvals"]:
@@ -584,14 +583,14 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha
)
approval_contents.append(approval_response)
chat_msg = ChatMessage(role=role, contents=approval_contents) # type: ignore[arg-type]
chat_msg = ChatMessage(role, approval_contents) # type: ignore[arg-type]
else:
# Regular text message
content = msg.get("content", "")
if isinstance(content, str):
chat_msg = ChatMessage(role=role, contents=[Content.from_text(text=content)])
chat_msg = ChatMessage(role, [Content.from_text(text=content)])
else:
chat_msg = ChatMessage(role=role, contents=[Content.from_text(text=str(content))])
chat_msg = ChatMessage(role, [Content.from_text(text=str(content))])
if "id" in msg:
chat_msg.message_id = msg["id"]
@@ -862,7 +862,7 @@ async def run_agent_stream(
from pydantic import BaseModel
logger.info(f"Processing structured output, update count: {len(all_updates)}")
final_response = AgentResponse.from_agent_run_response_updates(all_updates, output_format_type=response_format)
final_response = AgentResponse.from_updates(all_updates, output_format_type=response_format)
if final_response.value and isinstance(final_response.value, BaseModel):
response_dict = final_response.value.model_dump(mode="json", exclude_none=True)
@@ -10,19 +10,19 @@ from dataclasses import asdict, is_dataclass
from datetime import date, datetime
from typing import Any
from agent_framework import AgentResponseUpdate, ChatResponseUpdate, FunctionTool, Role, ToolProtocol
from agent_framework import AgentResponseUpdate, ChatResponseUpdate, FunctionTool, ToolProtocol
# Role mapping constants
AGUI_TO_FRAMEWORK_ROLE: dict[str, Role] = {
"user": Role.USER,
"assistant": Role.ASSISTANT,
"system": Role.SYSTEM,
AGUI_TO_FRAMEWORK_ROLE: dict[str, str] = {
"user": "user",
"assistant": "assistant",
"system": "system",
}
FRAMEWORK_TO_AGUI_ROLE: dict[Role, str] = {
Role.USER: "user",
Role.ASSISTANT: "assistant",
Role.SYSTEM: "system",
FRAMEWORK_TO_AGUI_ROLE: dict[str, str] = {
"user": "user",
"assistant": "assistant",
"system": "system",
}
ALLOWED_AGUI_ROLES: set[str] = {"user", "assistant", "system", "tool"}