mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
a4c9e43afb
commit
0521f5bed8
@@ -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
Reference in New Issue
Block a user