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
@@ -9,14 +9,14 @@ from typing import Any, Literal, cast
import yaml
from agent_framework import (
ChatAgent,
ChatClientProtocol,
Agent,
Content,
HostedCodeInterpreterTool,
HostedFileSearchTool,
HostedMCPSpecificApproval,
HostedMCPTool,
HostedWebSearchTool,
SupportsChatGetResponse,
ToolProtocol,
)
from agent_framework import (
@@ -124,10 +124,10 @@ class ProviderLookupError(DeclarativeLoaderError):
class AgentFactory:
"""Factory for creating ChatAgent instances from declarative YAML definitions.
"""Factory for creating Agent instances from declarative YAML definitions.
AgentFactory parses YAML agent definitions (PromptAgent kind) and creates
configured ChatAgent instances with the appropriate chat client, tools,
configured Agent instances with the appropriate chat client, tools,
and response format.
Examples:
@@ -150,7 +150,7 @@ class AgentFactory:
# With pre-configured chat client
client = AzureOpenAIChatClient()
factory = AgentFactory(chat_client=client)
factory = AgentFactory(client=client)
agent = factory.create_agent_from_yaml_path("agent.yaml")
.. code-block:: python
@@ -174,7 +174,7 @@ class AgentFactory:
def __init__(
self,
*,
chat_client: ChatClientProtocol | None = None,
client: SupportsChatGetResponse | None = None,
bindings: Mapping[str, Any] | None = None,
connections: Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Any] | None = None,
@@ -187,8 +187,8 @@ class AgentFactory:
"""Create the agent factory.
Args:
chat_client: An optional ChatClientProtocol instance to use as a dependency.
This will be passed to the ChatAgent that gets created.
client: An optional SupportsChatGetResponse instance to use as a dependency.
This will be passed to the Agent that gets created.
If you need to create multiple agents with different chat clients,
do not pass this and instead provide the chat client in the YAML definition.
bindings: An optional dictionary of bindings to use when creating agents.
@@ -210,9 +210,9 @@ class AgentFactory:
Here, "Provider.ApiType" is the lookup key used when both provider and apiType are specified in the
model, "Provider" is also allowed.
Package refers to which model needs to be imported, Name is the class name of the ChatClientProtocol
implementation, and model_id_field is the name of the field in the constructor
that accepts the model.id value.
Package refers to which model needs to be imported, Name is the class name of the
SupportsChatGetResponse implementation, and model_id_field is the name of the field in the
constructor that accepts the model.id value.
default_provider: The default provider used when model.provider is not specified,
default is "AzureAIClient".
safe_mode: Whether to run in safe mode, default is True.
@@ -241,7 +241,7 @@ class AgentFactory:
# With shared chat client
client = AzureOpenAIChatClient()
factory = AgentFactory(
chat_client=client,
client=client,
env_file_path=".env",
)
@@ -260,7 +260,7 @@ class AgentFactory:
},
)
"""
self.chat_client = chat_client
self.client = client
self.bindings = bindings
self.connections = connections
self.client_kwargs = client_kwargs or {}
@@ -269,8 +269,8 @@ class AgentFactory:
self.safe_mode = safe_mode
load_dotenv(dotenv_path=env_file_path, encoding=env_file_encoding)
def create_agent_from_yaml_path(self, yaml_path: str | Path) -> ChatAgent:
"""Create a ChatAgent from a YAML file path.
def create_agent_from_yaml_path(self, yaml_path: str | Path) -> Agent:
"""Create a Agent from a YAML file path.
This method does the following things:
@@ -278,13 +278,13 @@ class AgentFactory:
2. Validates that the loaded object is a PromptAgent.
3. Creates the appropriate ChatClient based on the model provider and apiType.
4. Parses the tools, options, and response format from the PromptAgent.
5. Creates and returns a ChatAgent instance with the configured properties.
5. Creates and returns a Agent instance with the configured properties.
Args:
yaml_path: Path to the YAML file representation of a PromptAgent.
Returns:
The ``ChatAgent`` instance created from the YAML file.
The ``Agent`` instance created from the YAML file.
Raises:
DeclarativeLoaderError: If the YAML does not represent a PromptAgent.
@@ -323,8 +323,8 @@ class AgentFactory:
yaml_str = f.read()
return self.create_agent_from_yaml(yaml_str)
def create_agent_from_yaml(self, yaml_str: str) -> ChatAgent:
"""Create a ChatAgent from a YAML string.
def create_agent_from_yaml(self, yaml_str: str) -> Agent:
"""Create a Agent from a YAML string.
This method does the following things:
@@ -332,13 +332,13 @@ class AgentFactory:
2. Validates that the loaded object is a PromptAgent.
3. Creates the appropriate ChatClient based on the model provider and apiType.
4. Parses the tools, options, and response format from the PromptAgent.
5. Creates and returns a ChatAgent instance with the configured properties.
5. Creates and returns a Agent instance with the configured properties.
Args:
yaml_str: YAML string representation of a PromptAgent.
Returns:
The ``ChatAgent`` instance created from the YAML string.
The ``Agent`` instance created from the YAML string.
Raises:
DeclarativeLoaderError: If the YAML does not represent a PromptAgent.
@@ -396,8 +396,8 @@ class AgentFactory:
"""
return self.create_agent_from_dict(yaml.safe_load(yaml_str))
def create_agent_from_dict(self, agent_def: dict[str, Any]) -> ChatAgent:
"""Create a ChatAgent from a dictionary definition.
def create_agent_from_dict(self, agent_def: dict[str, Any]) -> Agent:
"""Create a Agent from a dictionary definition.
This method does the following things:
@@ -405,13 +405,13 @@ class AgentFactory:
2. Validates that the loaded object is a PromptAgent.
3. Creates the appropriate ChatClient based on the model provider and apiType.
4. Parses the tools, options, and response format from the PromptAgent.
5. Creates and returns a ChatAgent instance with the configured properties.
5. Creates and returns a Agent instance with the configured properties.
Args:
agent_def: Dictionary representation of a PromptAgent.
Returns:
The `ChatAgent` instance created from the dictionary.
The `Agent` instance created from the dictionary.
Raises:
DeclarativeLoaderError: If the dictionary does not represent a PromptAgent.
@@ -454,16 +454,16 @@ class AgentFactory:
if output_schema := prompt_agent.outputSchema:
chat_options["response_format"] = _create_model_from_json_schema("agent", output_schema.to_json_schema())
# Step 3: Create the agent instance
return ChatAgent(
chat_client=client,
return Agent(
client=client,
name=prompt_agent.name,
description=prompt_agent.description,
instructions=prompt_agent.instructions,
**chat_options,
)
async def create_agent_from_yaml_path_async(self, yaml_path: str | Path) -> ChatAgent:
"""Async version: Create a ChatAgent from a YAML file path.
async def create_agent_from_yaml_path_async(self, yaml_path: str | Path) -> Agent:
"""Async version: Create a Agent from a YAML file path.
Use this method when the provider requires async initialization, such as
AzureAI.ProjectProvider which creates agents on the Azure AI Agent Service.
@@ -472,7 +472,7 @@ class AgentFactory:
yaml_path: Path to the YAML file representation of a PromptAgent.
Returns:
The ``ChatAgent`` instance created from the YAML file.
The ``Agent`` instance created from the YAML file.
Examples:
.. code-block:: python
@@ -492,8 +492,8 @@ class AgentFactory:
yaml_str = yaml_path.read_text()
return await self.create_agent_from_yaml_async(yaml_str)
async def create_agent_from_yaml_async(self, yaml_str: str) -> ChatAgent:
"""Async version: Create a ChatAgent from a YAML string.
async def create_agent_from_yaml_async(self, yaml_str: str) -> Agent:
"""Async version: Create a Agent from a YAML string.
Use this method when the provider requires async initialization, such as
AzureAI.ProjectProvider which creates agents on the Azure AI Agent Service.
@@ -502,7 +502,7 @@ class AgentFactory:
yaml_str: YAML string representation of a PromptAgent.
Returns:
The ``ChatAgent`` instance created from the YAML string.
The ``Agent`` instance created from the YAML string.
Examples:
.. code-block:: python
@@ -523,8 +523,8 @@ class AgentFactory:
"""
return await self.create_agent_from_dict_async(yaml.safe_load(yaml_str))
async def create_agent_from_dict_async(self, agent_def: dict[str, Any]) -> ChatAgent:
"""Async version: Create a ChatAgent from a dictionary definition.
async def create_agent_from_dict_async(self, agent_def: dict[str, Any]) -> Agent:
"""Async version: Create a Agent from a dictionary definition.
Use this method when the provider requires async initialization, such as
AzureAI.ProjectProvider which creates agents on the Azure AI Agent Service.
@@ -533,7 +533,7 @@ class AgentFactory:
agent_def: Dictionary representation of a PromptAgent.
Returns:
The ``ChatAgent`` instance created from the dictionary.
The ``Agent`` instance created from the dictionary.
Examples:
.. code-block:: python
@@ -571,20 +571,20 @@ class AgentFactory:
chat_options["tools"] = tools
if output_schema := prompt_agent.outputSchema:
chat_options["response_format"] = _create_model_from_json_schema("agent", output_schema.to_json_schema())
return ChatAgent(
chat_client=client,
return Agent(
client=client,
name=prompt_agent.name,
description=prompt_agent.description,
instructions=prompt_agent.instructions,
**chat_options,
)
async def _create_agent_with_provider(self, prompt_agent: PromptAgent, mapping: ProviderTypeMapping) -> ChatAgent:
"""Create a ChatAgent using AzureAIProjectAgentProvider.
async def _create_agent_with_provider(self, prompt_agent: PromptAgent, mapping: ProviderTypeMapping) -> Agent:
"""Create a Agent using AzureAIProjectAgentProvider.
This method handles the special case where we use a provider that creates
agents on a remote service (like Azure AI Agent Service) and returns
ChatAgent instances directly.
Agent instances directly.
"""
# Import the provider class
module_name = mapping["package"]
@@ -618,9 +618,9 @@ class AgentFactory:
response_format = _create_model_from_json_schema("agent", prompt_agent.outputSchema.to_json_schema())
# Create the agent using the provider
# The provider's create_agent returns a ChatAgent directly
# The provider's create_agent returns a Agent directly
return cast(
ChatAgent,
Agent,
await provider.create_agent(
name=prompt_agent.name,
model=prompt_agent.model.id if prompt_agent.model else None,
@@ -631,12 +631,12 @@ class AgentFactory:
),
)
def _get_client(self, prompt_agent: PromptAgent) -> ChatClientProtocol:
"""Create the ChatClientProtocol instance based on the PromptAgent model."""
def _get_client(self, prompt_agent: PromptAgent) -> SupportsChatGetResponse:
"""Create the SupportsChatGetResponse instance based on the PromptAgent model."""
if not prompt_agent.model:
# if no model is defined, use the supplied chat_client
if self.chat_client:
return self.chat_client
# if no model is defined, use the supplied client
if self.client:
return self.client
raise DeclarativeLoaderError(
"ChatClient must be provided to create agent from PromptAgent, "
"alternatively define a model in the PromptAgent."
@@ -670,9 +670,9 @@ class AgentFactory:
# Any client we create, needs a model.id
if not prompt_agent.model.id:
# if prompt_agent.model is defined, but no id, use the supplied chat_client
if self.chat_client:
return self.chat_client
# if prompt_agent.model is defined, but no id, use the supplied client
if self.client:
return self.client
# or raise, since we cannot create a client without model id
raise DeclarativeLoaderError(
"ChatClient must be provided to create agent from PromptAgent, or define model.id in the PromptAgent."
@@ -14,7 +14,7 @@ from collections.abc import AsyncGenerator
from typing import Any, cast
from agent_framework import get_logger
from agent_framework._types import AgentResponse, ChatMessage
from agent_framework._types import AgentResponse, Message
from ._handlers import (
ActionContext,
@@ -162,7 +162,7 @@ def _extract_json_from_response(text: str) -> Any:
raise json.JSONDecodeError("No valid JSON found in response", text, 0)
def _build_messages_from_state(ctx: ActionContext) -> list[ChatMessage]:
def _build_messages_from_state(ctx: ActionContext) -> list[Message]:
"""Build the message list to send to an agent.
This collects messages from:
@@ -174,9 +174,9 @@ def _build_messages_from_state(ctx: ActionContext) -> list[ChatMessage]:
ctx: The action context
Returns:
List of ChatMessage objects to send to the agent
List of Message objects to send to the agent
"""
messages: list[ChatMessage] = []
messages: list[Message] = []
# Get conversation history
history = ctx.state.get("conversation.messages", [])
@@ -287,23 +287,23 @@ async def handle_invoke_azure_agent(ctx: ActionContext) -> AsyncGenerator[Workfl
evaluated_input = ctx.state.eval_if_expression(input_messages)
if evaluated_input:
if isinstance(evaluated_input, str):
messages.append(ChatMessage(role="user", text=evaluated_input))
messages.append(Message(role="user", text=evaluated_input))
elif isinstance(evaluated_input, list):
for msg_item in evaluated_input: # type: ignore
if isinstance(msg_item, str):
messages.append(ChatMessage(role="user", text=msg_item))
elif isinstance(msg_item, ChatMessage):
messages.append(Message(role="user", text=msg_item))
elif isinstance(msg_item, Message):
messages.append(msg_item)
elif isinstance(msg_item, dict) and "content" in msg_item:
item_dict = cast(dict[str, Any], msg_item)
role: str = str(item_dict.get("role", "user"))
content: str = str(item_dict.get("content", ""))
if role == "user":
messages.append(ChatMessage(role="user", text=content))
messages.append(Message(role="user", text=content))
elif role == "assistant":
messages.append(ChatMessage(role="assistant", text=content))
messages.append(Message(role="assistant", text=content))
elif role == "system":
messages.append(ChatMessage(role="system", text=content))
messages.append(Message(role="system", text=content))
# Evaluate and include input arguments
evaluated_args: dict[str, Any] = {}
@@ -365,7 +365,7 @@ async def handle_invoke_azure_agent(ctx: ActionContext) -> AsyncGenerator[Workfl
# Add to conversation history
if text:
ctx.state.add_conversation_message(ChatMessage(role="assistant", text=text))
ctx.state.add_conversation_message(Message(role="assistant", text=text))
# Store in output variables (.NET style)
if output_messages_var:
@@ -418,7 +418,7 @@ async def handle_invoke_azure_agent(ctx: ActionContext) -> AsyncGenerator[Workfl
# Add to conversation history
if text:
ctx.state.add_conversation_message(ChatMessage(role="assistant", text=text))
ctx.state.add_conversation_message(Message(role="assistant", text=text))
# Store in output variables (.NET style)
if output_messages_var:
@@ -564,8 +564,8 @@ async def handle_invoke_prompt_agent(ctx: ActionContext) -> AsyncGenerator[Workf
# Add input as user message if provided
if input_value:
if isinstance(input_value, str):
messages.append(ChatMessage(role="user", text=input_value))
elif isinstance(input_value, ChatMessage):
messages.append(Message(role="user", text=input_value))
elif isinstance(input_value, Message):
messages.append(input_value)
logger.debug(f"InvokePromptAgent: calling '{agent_name}' with {len(messages)} messages")
@@ -594,7 +594,7 @@ async def handle_invoke_prompt_agent(ctx: ActionContext) -> AsyncGenerator[Workf
ctx.state.set_agent_result(text=text, messages=response_messages)
if text:
ctx.state.add_conversation_message(ChatMessage(role="assistant", text=text))
ctx.state.add_conversation_message(Message(role="assistant", text=text))
if output_path:
ctx.state.set(output_path, text)
@@ -614,7 +614,7 @@ async def handle_invoke_prompt_agent(ctx: ActionContext) -> AsyncGenerator[Workf
ctx.state.set_agent_result(text=text, messages=response_messages)
if text:
ctx.state.add_conversation_message(ChatMessage(role="assistant", text=text))
ctx.state.add_conversation_message(Message(role="assistant", text=text))
if output_path:
ctx.state.set(output_path, text)
@@ -102,7 +102,7 @@ def _make_powerfx_safe(value: Any) -> Any:
"""Convert a value to a PowerFx-serializable form.
PowerFx can only serialize primitive types, dicts, and lists.
Custom objects (like ChatMessage) must be converted to dicts or excluded.
Custom objects (like Message) must be converted to dicts or excluded.
Args:
value: Any Python value
@@ -558,8 +558,8 @@ class DeclarativeWorkflowState:
# Try "text" key first (simple dict format)
if "text" in last_msg:
return str(last_msg["text"])
# Try extracting from "contents" (ChatMessage dict format)
# ChatMessage.text concatenates text from all TextContent items
# Try extracting from "contents" (Message dict format)
# Message.text concatenates text from all TextContent items
contents = last_msg.get("contents", [])
if isinstance(contents, list):
text_parts = []
@@ -20,8 +20,8 @@ from dataclasses import dataclass, field
from typing import Any, cast
from agent_framework import (
ChatMessage,
Content,
Message,
WorkflowContext,
handler,
response_handler,
@@ -170,7 +170,7 @@ def _extract_json_from_response(text: str) -> Any:
raise json.JSONDecodeError("No valid JSON found in response", text, 0)
def _validate_conversation_history(messages: list[ChatMessage], agent_name: str) -> None:
def _validate_conversation_history(messages: list[Message], agent_name: str) -> None:
"""Validate that conversation history has matching tool calls and results.
This helps catch issues where tool call messages are stored without their
@@ -263,7 +263,7 @@ class AgentResult:
success: bool
response: str
agent_name: str
messages: list[ChatMessage] = field(default_factory=lambda: cast(list[ChatMessage], []))
messages: list[Message] = field(default_factory=lambda: cast(list[Message], []))
tool_calls: list[Content] = field(default_factory=lambda: cast(list[Content], []))
error: str | None = None
@@ -309,7 +309,7 @@ class AgentExternalInputRequest:
agent_name: str
agent_response: str
iteration: int = 0
messages: list[ChatMessage] = field(default_factory=lambda: cast(list[ChatMessage], []))
messages: list[Message] = field(default_factory=lambda: cast(list[Message], []))
function_calls: list[Content] = field(default_factory=lambda: cast(list[Content], []))
@@ -340,7 +340,7 @@ class AgentExternalInputResponse:
"""
user_input: str
messages: list[ChatMessage] = field(default_factory=lambda: cast(list[ChatMessage], []))
messages: list[Message] = field(default_factory=lambda: cast(list[Message], []))
function_results: dict[str, Content] = field(default_factory=lambda: cast(dict[str, Content], {}))
@@ -637,20 +637,20 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
Tuple of (accumulated_response, all_messages, tool_calls)
"""
accumulated_response = ""
all_messages: list[ChatMessage] = []
all_messages: list[Message] = []
tool_calls: list[Content] = []
# Add user input to conversation history first (via state.append only)
if input_text:
user_message = ChatMessage(role="user", text=input_text)
user_message = Message(role="user", text=input_text)
state.append(messages_path, user_message)
# Get conversation history from state AFTER adding user message
# Note: We get a fresh copy to avoid mutation issues
conversation_history: list[ChatMessage] = state.get(messages_path) or []
conversation_history: list[Message] = state.get(messages_path) or []
# Build messages list for agent (use history if available, otherwise just input)
messages_for_agent: list[ChatMessage] | str = conversation_history if conversation_history else input_text
messages_for_agent: list[Message] | str = conversation_history if conversation_history else input_text
# Validate conversation history before invoking agent
if isinstance(messages_for_agent, list) and messages_for_agent:
@@ -672,7 +672,7 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
if not isinstance(result, str):
result_messages: Any = getattr(result, "messages", None)
if result_messages is not None:
all_messages = list(cast(list[ChatMessage], result_messages))
all_messages = list(cast(list[Message], result_messages))
result_tool_calls: Any = getattr(result, "tool_calls", None)
if result_tool_calls is not None:
tool_calls = list(cast(list[Content], result_tool_calls))
@@ -707,7 +707,7 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
"Agent '%s': No messages in response, creating simple assistant message",
agent_name,
)
assistant_message = ChatMessage(role="assistant", text=accumulated_response)
assistant_message = Message(role="assistant", text=accumulated_response)
state.append(messages_path, assistant_message)
# Store results in state - support both schema formats:
@@ -73,8 +73,8 @@ class WorkflowFactory:
from agent_framework.declarative import WorkflowFactory
# Pre-register agents for InvokeAzureAgent actions
chat_client = AzureOpenAIChatClient()
agent = chat_client.as_agent(name="MyAgent", instructions="You are helpful.")
client = AzureOpenAIChatClient()
agent = client.as_agent(name="MyAgent", instructions="You are helpful.")
factory = WorkflowFactory(agents={"MyAgent": agent})
workflow = factory.create_workflow_from_yaml_path("workflow.yaml")
@@ -517,7 +517,7 @@ class WorkflowFactory:
Args:
name: The name to register the agent under. Must match the agent name
referenced in InvokeAzureAgent actions.
agent: The agent instance (typically a ChatAgent or similar).
agent: The agent instance (typically a Agent or similar).
Returns:
Self for method chaining.
@@ -314,7 +314,7 @@ class WorkflowState:
"""Add a message to the conversation history.
Args:
message: The message to add (typically a ChatMessage or similar)
message: The message to add (typically a Message or similar)
"""
self._conversation["messages"].append(message)
self._conversation["history"].append(message)