Python: [BREAKING] Simplify API: ChatAgent -> Agent, ChatMessage -> Message (#3747)

* [BREAKING] Rename ChatAgent -> Agent, ChatMessage -> Message, ChatClientProtocol -> SupportsChatGetResponse

Simplify the public API by removing redundant 'Chat' prefix from core types:
- ChatAgent -> Agent
- RawChatAgent -> RawAgent
- ChatMessage -> Message
- ChatClientProtocol -> SupportsChatGetResponse

Also renamed internal WorkflowMessage (was Message in _runner_context) to avoid collision.

No backward compatibility aliases - this is a clean breaking change.

* [BREAKING] Rename Agent chat_client parameter to client

* Fix rebase issues: WorkflowMessage references and broken markdown links

* Fix formatting and lint issues from code quality checks

* Fix import ordering in workflow sample files

* fixed rebase

* Fix test failures: use WorkflowMessage and A2AMessage after ChatMessage→Message rename

- Replace Message(data=..., source_id=...) with WorkflowMessage(...) in workflow tests
- Fix isinstance check in A2A agent to use A2AMessage instead of Message
- Fix import in test_workflow_observability.py (Message→WorkflowMessage)

* Fix lint, fmt, and sample errors after ChatMessage→Message rename

- Auto-fix 70+ ruff lint issues across samples (ChatMessage→Message refs)
- Fix HostedVectorStoreContent→Content.from_hosted_vector_store in file search sample
- Fix _normalize_messages→normalize_messages in custom agent sample
- Fix context.terminate→raise MiddlewareTermination in middleware samples
- Fix with_update_hook→with_transform_hook in override middleware sample
- Add TOptions_co import back to custom_chat_client sample
- Add noqa for FastAPI File() default in chatkit sample
- Fix B023 loop variable capture in weather agent sample

* fix: update Agent constructor calls from chat_client to client in declaration-only tool tests

* fix: add register_cleanup to devui lazy-loading proxy and type stub

* fixed tests and updated new pieces

* fix agui typevar

* fix merge errors

* fix merge conflicts

* fiux merge

* Remove unused links

---------

Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
This commit is contained in:
Eduard van Valkenburg
2026-02-11 00:04:32 +01:00
committed by GitHub
Unverified
parent a4c9e43afb
commit 0521f5bed8
418 changed files with 5385 additions and 5389 deletions
@@ -41,7 +41,7 @@ def register_cleanup(entity: Any, *hooks: Callable[[], Any]) -> None:
Single cleanup hook:
>>> from agent_framework.devui import serve, register_cleanup
>>> credential = DefaultAzureCredential()
>>> agent = ChatAgent(...)
>>> agent = Agent(...)
>>> register_cleanup(agent, credential.close)
>>> serve(entities=[agent])
@@ -52,7 +52,7 @@ def register_cleanup(entity: Any, *hooks: Callable[[], Any]) -> None:
>>> # In agents/my_agent/agent.py
>>> from agent_framework.devui import register_cleanup
>>> credential = DefaultAzureCredential()
>>> agent = ChatAgent(...)
>>> agent = Agent(...)
>>> register_cleanup(agent, credential.close)
>>> # Run: devui ./agents
"""
@@ -13,11 +13,11 @@ import uuid
from abc import ABC, abstractmethod
from typing import Any, Literal, cast
from agent_framework import AgentThread, ChatMessage
from agent_framework import AgentThread, Message
from agent_framework._workflows._checkpoint import InMemoryCheckpointStorage
from openai.types.conversations import Conversation, ConversationDeletedResource
from openai.types.conversations.conversation_item import ConversationItem
from openai.types.conversations.message import Message
from openai.types.conversations.message import Message as OpenAIMessage
from openai.types.conversations.text_content import TextContent
from openai.types.responses import (
ResponseFunctionToolCallItem,
@@ -305,7 +305,7 @@ class InMemoryConversationStore(ConversationStore):
content = item.get("content", [])
text = content[0].get("text", "") if content else ""
chat_msg = ChatMessage(role=role, text=text) # type: ignore[arg-type]
chat_msg = Message(role=role, text=text) # type: ignore[arg-type]
chat_messages.append(chat_msg)
# Add messages to AgentThread
@@ -320,7 +320,7 @@ class InMemoryConversationStore(ConversationStore):
role_str = msg.role if hasattr(msg.role, "value") else str(msg.role)
role = cast(MessageRole, role_str) # Safe: Agent Framework roles match OpenAI roles
# Convert ChatMessage contents to OpenAI TextContent format
# Convert Message contents to OpenAI TextContent format
message_content = []
for content_item in msg.contents:
if content_item.type == "text":
@@ -329,7 +329,7 @@ class InMemoryConversationStore(ConversationStore):
message_content.append(TextContent(type="text", text=text_value))
# Create Message object (concrete type from ConversationItem union)
message = Message(
message = OpenAIMessage(
id=item_id,
type="message", # Required discriminator for union
role=role,
@@ -372,14 +372,14 @@ class InMemoryConversationStore(ConversationStore):
if thread.message_store:
af_messages = await thread.message_store.list_messages()
# Convert each AgentFramework ChatMessage to appropriate ConversationItem type(s)
# Convert each AgentFramework Message to appropriate ConversationItem type(s)
for i, msg in enumerate(af_messages):
item_id = f"item_{i}"
role_str = msg.role if hasattr(msg.role, "value") else str(msg.role)
role = cast(MessageRole, role_str) # Safe: Agent Framework roles match OpenAI roles
# Process each content item in the message
# A single ChatMessage may produce multiple ConversationItems
# A single Message may produce multiple ConversationItems
# (e.g., a message with both text and a function call)
message_contents: list[TextContent | ResponseInputImage | ResponseInputFile] = []
function_calls = []
@@ -464,7 +464,7 @@ class InMemoryConversationStore(ConversationStore):
# Create ConversationItems based on what we found
# If message has text/images/files, create a Message item
if message_contents:
message = Message(
message = OpenAIMessage(
id=item_id,
type="message",
role=role, # type: ignore
@@ -541,7 +541,7 @@ class EntityDiscovery:
"""Check if a Python file has entity exports (agent or workflow) using AST parsing.
This safely checks for module-level assignments like:
- agent = ChatAgent(...)
- agent = Agent(...)
- workflow = WorkflowBuilder(start_executor=...)...
Args:
@@ -305,7 +305,7 @@ class AgentFrameworkExecutor:
yield AgentStartedEvent()
# Convert input to proper ChatMessage or string
# Convert input to proper Message or string
user_message = self._convert_input_to_chat_message(request.input)
# Get thread from conversation parameter (OpenAI standard!)
@@ -321,7 +321,7 @@ class AgentFrameworkExecutor:
if isinstance(user_message, str):
logger.debug(f"Executing agent with text input: {user_message[:100]}...")
else:
logger.debug(f"Executing agent with multimodal ChatMessage: {type(user_message)}")
logger.debug(f"Executing agent with multimodal Message: {type(user_message)}")
# Workaround for MCP tool stale connection bug (GitHub issue pending)
# When HTTP streaming ends, GeneratorExit can close MCP stdio streams
@@ -534,7 +534,7 @@ class AgentFrameworkExecutor:
yield {"type": "error", "message": f"Workflow execution error: {e!s}"}
def _convert_input_to_chat_message(self, input_data: Any) -> Any:
"""Convert OpenAI Responses API input to Agent Framework ChatMessage or string.
"""Convert OpenAI Responses API input to Agent Framework Message or string.
Handles various input formats including text, images, files, and multimodal content.
Falls back to string extraction for simple cases.
@@ -543,11 +543,11 @@ class AgentFrameworkExecutor:
input_data: OpenAI ResponseInputParam (List[ResponseInputItemParam])
Returns:
ChatMessage for multimodal content, or string for simple text
Message for multimodal content, or string for simple text
"""
# Import Agent Framework types
try:
from agent_framework import ChatMessage, Role
from agent_framework import Message, Role
except ImportError:
# Fallback to string extraction if Agent Framework not available
return self._extract_user_message_fallback(input_data)
@@ -558,24 +558,24 @@ class AgentFrameworkExecutor:
# Handle OpenAI ResponseInputParam (List[ResponseInputItemParam])
if isinstance(input_data, list):
return self._convert_openai_input_to_chat_message(input_data, ChatMessage, Role)
return self._convert_openai_input_to_chat_message(input_data, Message, Role)
# Fallback for other formats
return self._extract_user_message_fallback(input_data)
def _convert_openai_input_to_chat_message(self, input_items: list[Any], ChatMessage: Any, Role: Any) -> Any:
"""Convert OpenAI ResponseInputParam to Agent Framework ChatMessage.
def _convert_openai_input_to_chat_message(self, input_items: list[Any], Message: Any, Role: Any) -> Any:
"""Convert OpenAI ResponseInputParam to Agent Framework Message.
Processes text, images, files, and other content types from OpenAI format
to Agent Framework ChatMessage with appropriate content objects.
to Agent Framework Message with appropriate content objects.
Args:
input_items: List of OpenAI ResponseInputItemParam objects (dicts or objects)
ChatMessage: ChatMessage class for creating chat messages
Message: Message class for creating chat messages
Role: Role enum for message roles
Returns:
ChatMessage with converted content
Message with converted content
"""
contents: list[Content] = []
@@ -705,9 +705,9 @@ class AgentFrameworkExecutor:
if not contents:
contents.append(Content.from_text(text=""))
chat_message = ChatMessage(role="user", contents=contents)
chat_message = Message(role="user", contents=contents)
logger.info(f"Created ChatMessage with {len(contents)} contents:")
logger.info(f"Created Message with {len(contents)} contents:")
for idx, content in enumerate(contents):
content_type = content.__class__.__name__
if hasattr(content, "media_type"):
@@ -772,9 +772,9 @@ class AgentFrameworkExecutor:
pass
# Check for OpenAI multimodal format (list with type: "message")
# This handles ChatMessage inputs with images, files, etc.
# This handles Message inputs with images, files, etc.
if self._is_openai_multimodal_format(raw_input):
logger.debug("Detected OpenAI multimodal format, converting to ChatMessage")
logger.debug("Detected OpenAI multimodal format, converting to Message")
return self._convert_input_to_chat_message(raw_input)
# Handle structured input (dict)
@@ -14,7 +14,7 @@ from datetime import datetime
from typing import Any, Union
from uuid import uuid4
from agent_framework import ChatMessage, Content
from agent_framework import Content, Message
from openai.types.responses import (
Response,
ResponseContentPartAddedEvent,
@@ -453,7 +453,7 @@ class MessageMapper:
Handles:
- Primitives (str, int, float, bool, None)
- Collections (list, tuple, set, dict)
- SerializationMixin objects (ChatMessage, etc.) - calls to_dict()
- SerializationMixin objects (Message, etc.) - calls to_dict()
- Pydantic models - calls model_dump()
- Dataclasses - recursively serializes with asdict()
- Enums - extracts value
@@ -502,7 +502,7 @@ class MessageMapper:
if isinstance(value, dict):
return {k: self._serialize_value(v) for k, v in value.items()}
# Handle SerializationMixin (like ChatMessage) - call to_dict()
# Handle SerializationMixin (like Message) - call to_dict()
if hasattr(value, "to_dict") and callable(getattr(value, "to_dict", None)):
try:
return value.to_dict() # type: ignore[attr-defined, no-any-return]
@@ -536,7 +536,7 @@ class MessageMapper:
def _serialize_request_data(self, request_data: Any) -> dict[str, Any]:
"""Serialize RequestInfoMessage to dict for JSON transmission.
Handles nested SerializationMixin objects (like ChatMessage) within dataclasses.
Handles nested SerializationMixin objects (like Message) within dataclasses.
Args:
request_data: The RequestInfoMessage instance
@@ -554,7 +554,7 @@ class MessageMapper:
return {k: self._serialize_value(v) for k, v in request_data.items()}
# Handle dataclasses with nested SerializationMixin objects
# We can't use asdict() directly because it doesn't handle ChatMessage
# We can't use asdict() directly because it doesn't handle Message
if is_dataclass(request_data) and not isinstance(request_data, type):
try:
# Manually serialize each field to handle nested SerializationMixin
@@ -892,17 +892,17 @@ class MessageMapper:
# Extract text from output data based on type
text = None
if isinstance(output_data, ChatMessage):
# Handle ChatMessage (from Magentic and AgentExecutor with output_response=True)
if isinstance(output_data, Message):
# Handle Message (from Magentic and AgentExecutor with output_response=True)
text = getattr(output_data, "text", None)
if not text:
# Fallback to string representation
text = str(output_data)
elif isinstance(output_data, list):
# Handle list of ChatMessage objects (from Magentic yield_output([final_answer]))
# Handle list of Message objects (from Magentic yield_output([final_answer]))
text_parts = []
for item in output_data:
if isinstance(item, ChatMessage):
if isinstance(item, Message):
item_text = getattr(item, "text", None)
if item_text:
text_parts.append(item_text)
@@ -1047,7 +1047,7 @@ class MessageMapper:
# Create ExecutorActionItem with completed status
# executor_completed event (type='executor_completed') uses 'data' field, not 'result'
# Serialize the result data to ensure it's JSON-serializable
# (AgentExecutorResponse contains AgentResponse/ChatMessage which are SerializationMixin)
# (AgentExecutorResponse contains AgentResponse/Message which are SerializationMixin)
raw_result = getattr(event, "data", None)
serialized_result = self._serialize_value(raw_result) if raw_result is not None else None
executor_item = ExecutorActionItem(
@@ -222,8 +222,8 @@ class DevServer:
# Step 2: Close chat clients and their credentials (EXISTING)
entity_obj = self.executor.entity_discovery.get_entity_object(entity_id)
if entity_obj and hasattr(entity_obj, "chat_client"):
client = entity_obj.chat_client
if entity_obj and hasattr(entity_obj, "client"):
client = entity_obj.client
# Close the chat client itself
if hasattr(client, "close") and callable(client.close):
@@ -9,7 +9,7 @@ from dataclasses import fields, is_dataclass
from types import UnionType
from typing import Any, Union, get_args, get_origin, get_type_hints
from agent_framework import ChatMessage
from agent_framework import Message
logger = logging.getLogger(__name__)
@@ -45,7 +45,7 @@ def extract_agent_metadata(entity_object: Any) -> dict[str, Any]:
elif hasattr(chat_opts, "instructions"):
metadata["instructions"] = chat_opts.instructions
# Try to get model - check both default_options and chat_client
# Try to get model - check both default_options and client
if hasattr(entity_object, "default_options"):
chat_opts = entity_object.default_options
if isinstance(chat_opts, dict):
@@ -53,16 +53,12 @@ def extract_agent_metadata(entity_object: Any) -> dict[str, Any]:
metadata["model"] = chat_opts.get("model_id")
elif hasattr(chat_opts, "model_id") and chat_opts.model_id:
metadata["model"] = chat_opts.model_id
if (
metadata["model"] is None
and hasattr(entity_object, "chat_client")
and hasattr(entity_object.chat_client, "model_id")
):
metadata["model"] = entity_object.chat_client.model_id
if metadata["model"] is None and hasattr(entity_object, "client") and hasattr(entity_object.client, "model_id"):
metadata["model"] = entity_object.client.model_id
# Try to get chat client type
if hasattr(entity_object, "chat_client"):
metadata["chat_client_type"] = entity_object.chat_client.__class__.__name__
if hasattr(entity_object, "client"):
metadata["chat_client_type"] = entity_object.client.__class__.__name__
# Try to get context providers
if (
@@ -124,8 +120,8 @@ def extract_executor_message_types(executor: Any) -> list[Any]:
def _contains_chat_message(type_hint: Any) -> bool:
"""Check whether the provided type hint directly or indirectly references ChatMessage."""
if type_hint is ChatMessage:
"""Check whether the provided type hint directly or indirectly references Message."""
if type_hint is Message:
return True
origin = get_origin(type_hint)
@@ -141,7 +137,7 @@ def _contains_chat_message(type_hint: Any) -> bool:
def select_primary_input_type(message_types: list[Any]) -> Any | None:
"""Choose the most user-friendly input type for workflow inputs.
Prefers ChatMessage (or containers thereof) and then falls back to primitives.
Prefers Message (or containers thereof) and then falls back to primitives.
Args:
message_types: List of possible message types
@@ -154,7 +150,7 @@ def select_primary_input_type(message_types: list[Any]) -> Any | None:
for message_type in message_types:
if _contains_chat_message(message_type):
return ChatMessage
return Message
preferred = (str, dict)
@@ -427,7 +423,7 @@ def generate_input_schema(input_type: type) -> dict[str, Any]:
if hasattr(input_type, "model_json_schema"):
return input_type.model_json_schema() # type: ignore
# 3. SerializationMixin classes (ChatMessage, etc.)
# 3. SerializationMixin classes (Message, etc.)
if is_serialization_mixin(input_type):
return generate_schema_from_serialization_mixin(input_type)
@@ -521,7 +517,7 @@ def _parse_string_input(input_str: str, target_type: type) -> Any:
except Exception as e:
logger.debug(f"Failed to parse string as Pydantic model: {e}")
# SerializationMixin (like ChatMessage)
# SerializationMixin (like Message)
if is_serialization_mixin(target_type):
try:
# Try parsing as JSON dict first
@@ -531,7 +527,7 @@ def _parse_string_input(input_str: str, target_type: type) -> Any:
return target_type.from_dict(data) # type: ignore
return target_type(**data) # type: ignore
# For ChatMessage specifically: create from text
# For Message specifically: create from text
# Try common field patterns
common_fields = ["text", "message", "content"]
sig = inspect.signature(target_type)
File diff suppressed because one or more lines are too long