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
@@ -20,7 +20,7 @@ Interactive developer UI for testing and debugging agents and workflows.
|
||||
```python
|
||||
from agent_framework.devui import serve
|
||||
|
||||
agent = ChatAgent(...)
|
||||
agent = Agent(...)
|
||||
serve(entities=[agent], port=8080, auto_open=True)
|
||||
```
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ pip install agent-framework-devui --pre
|
||||
You can also launch it programmatically
|
||||
|
||||
```python
|
||||
from agent_framework import ChatAgent
|
||||
from agent_framework import Agent
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from agent_framework.devui import serve
|
||||
|
||||
@@ -26,9 +26,9 @@ def get_weather(location: str) -> str:
|
||||
return f"Weather in {location}: 72°F and sunny"
|
||||
|
||||
# Create your agent
|
||||
agent = ChatAgent(
|
||||
agent = Agent(
|
||||
name="WeatherAgent",
|
||||
chat_client=OpenAIChatClient(),
|
||||
client=OpenAIChatClient(),
|
||||
tools=[get_weather]
|
||||
)
|
||||
|
||||
@@ -55,8 +55,8 @@ When DevUI starts with no discovered entities, it displays a **sample entity gal
|
||||
|
||||
```python
|
||||
# ✅ Correct - DevUI handles cleanup automatically
|
||||
mcp_tool = MCPStreamableHTTPTool(url="http://localhost:8011/mcp", chat_client=chat_client)
|
||||
agent = ChatAgent(tools=mcp_tool)
|
||||
mcp_tool = MCPStreamableHTTPTool(url="http://localhost:8011/mcp", client=client)
|
||||
agent = Agent(tools=mcp_tool)
|
||||
serve(entities=[agent])
|
||||
```
|
||||
|
||||
@@ -68,13 +68,13 @@ Register cleanup hooks to properly close credentials and resources on shutdown:
|
||||
|
||||
```python
|
||||
from azure.identity.aio import DefaultAzureCredential
|
||||
from agent_framework import ChatAgent
|
||||
from agent_framework import Agent
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework_devui import register_cleanup, serve
|
||||
|
||||
credential = DefaultAzureCredential()
|
||||
client = AzureOpenAIChatClient()
|
||||
agent = ChatAgent(name="MyAgent", chat_client=client)
|
||||
agent = Agent(name="MyAgent", client=client)
|
||||
|
||||
# Register cleanup hook - credential will be closed on shutdown
|
||||
register_cleanup(agent, credential.close)
|
||||
@@ -92,7 +92,7 @@ For your agents to be discovered by the DevUI, they must be organized in a direc
|
||||
```
|
||||
agents/
|
||||
├── weather_agent/
|
||||
│ ├── __init__.py # Must export: agent = ChatAgent(...)
|
||||
│ ├── __init__.py # Must export: agent = Agent(...)
|
||||
│ ├── agent.py
|
||||
│ └── .env # Optional: API keys, config vars
|
||||
├── my_workflow/
|
||||
|
||||
@@ -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
+1
-1
@@ -76,7 +76,7 @@ export function RunWorkflowButton({
|
||||
|
||||
// Analyze input requirements
|
||||
const inputAnalysis = useMemo(() => {
|
||||
// Check if this is a ChatMessage schema (for AgentExecutor workflows)
|
||||
// Check if this is a Message schema (for AgentExecutor workflows)
|
||||
const isChatMessage = isChatMessageSchema(inputSchema);
|
||||
|
||||
if (!inputSchema)
|
||||
|
||||
+3
-3
@@ -115,7 +115,7 @@ export function getFieldColumnSpan(
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ChatMessage Pattern Detection (exported for reuse)
|
||||
// Message Pattern Detection (exported for reuse)
|
||||
// ============================================================================
|
||||
|
||||
export function detectChatMessagePattern(
|
||||
@@ -436,7 +436,7 @@ export function SchemaFormRenderer({
|
||||
(name) => !hideFields.includes(name)
|
||||
);
|
||||
|
||||
// Detect ChatMessage pattern
|
||||
// Detect Message pattern
|
||||
const isChatMessageLike = detectChatMessagePattern(schema, requiredFields);
|
||||
|
||||
// Separate required and optional fields
|
||||
@@ -449,7 +449,7 @@ export function SchemaFormRenderer({
|
||||
(name) => !requiredFields.includes(name)
|
||||
);
|
||||
|
||||
// For ChatMessage: prioritize text/message/content
|
||||
// For Message: prioritize text/message/content
|
||||
const sortedOptionalFields = isChatMessageLike
|
||||
? [...optionalFieldNames].sort((a, b) => {
|
||||
const priority = (name: string) =>
|
||||
|
||||
+2
-2
@@ -48,7 +48,7 @@ export function WorkflowInputForm({
|
||||
const requiredFields = inputSchema.required || [];
|
||||
const isSimpleInput = inputSchema.type === "string" && !inputSchema.enum;
|
||||
|
||||
// Detect ChatMessage-like pattern for auto-filling role
|
||||
// Detect Message-like pattern for auto-filling role
|
||||
const isChatMessageLike = detectChatMessagePattern(inputSchema, requiredFields);
|
||||
|
||||
// Validation: check if required fields are filled
|
||||
@@ -82,7 +82,7 @@ export function WorkflowInputForm({
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-fill role="user" for ChatMessage-like inputs
|
||||
// Auto-fill role="user" for Message-like inputs
|
||||
if (isChatMessageLike && !initialData["role"]) {
|
||||
initialData["role"] = "user";
|
||||
}
|
||||
|
||||
@@ -1340,7 +1340,7 @@ function TraceTreeNode({ node, depth = 0 }: { node: TraceNode; depth?: number })
|
||||
|
||||
{/* Operation badge */}
|
||||
<span className={`text-xs px-1.5 py-0.5 rounded font-medium ${getOperationColor(operationName)}`}>
|
||||
{operationName.replace("ChatAgent.", "").replace("invoke_agent ", "")}
|
||||
{operationName.replace("Agent.", "").replace("invoke_agent ", "")}
|
||||
</span>
|
||||
|
||||
{/* Duration */}
|
||||
|
||||
@@ -223,7 +223,7 @@ export interface AgentResponseUpdate {
|
||||
|
||||
// Agent run response (final)
|
||||
export interface AgentResponse {
|
||||
messages: ChatMessage[];
|
||||
messages: Message[];
|
||||
response_id?: string;
|
||||
created_at?: CreatedAtT;
|
||||
usage_details?: UsageDetails;
|
||||
@@ -232,7 +232,7 @@ export interface AgentResponse {
|
||||
}
|
||||
|
||||
// Chat message
|
||||
export interface ChatMessage {
|
||||
export interface Message {
|
||||
contents: Content[];
|
||||
role?: Role;
|
||||
author_name?: string;
|
||||
|
||||
@@ -185,7 +185,7 @@ export interface MetaResponse {
|
||||
}
|
||||
|
||||
// Chat message types matching Agent Framework
|
||||
export interface ChatMessage {
|
||||
export interface Message {
|
||||
id: string;
|
||||
role: "user" | "assistant" | "system" | "tool";
|
||||
contents: import("./agent-framework").Content[];
|
||||
@@ -212,7 +212,7 @@ export interface AppState {
|
||||
}
|
||||
|
||||
export interface ChatState {
|
||||
messages: ChatMessage[];
|
||||
messages: Message[];
|
||||
isStreaming: boolean;
|
||||
// streamEvents removed - use OpenAI events directly instead
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ import type { Workflow } from "@/types/workflow";
|
||||
import { getTypedWorkflow } from "@/types/workflow";
|
||||
|
||||
/**
|
||||
* Detects if a JSON schema represents a ChatMessage input type.
|
||||
* ChatMessage schemas typically have:
|
||||
* Detects if a JSON schema represents a Message input type.
|
||||
* Message schemas typically have:
|
||||
* - type: "object"
|
||||
* - properties with "text" (required string) and "role" (optional string)
|
||||
*
|
||||
@@ -24,7 +24,7 @@ import { getTypedWorkflow } from "@/types/workflow";
|
||||
* component for workflows that start with an AgentExecutor.
|
||||
*
|
||||
* @param schema - The JSON schema to check
|
||||
* @returns true if the schema represents a ChatMessage-like input
|
||||
* @returns true if the schema represents a Message-like input
|
||||
*/
|
||||
export function isChatMessageSchema(schema: JSONSchemaProperty | undefined): boolean {
|
||||
if (!schema) return false;
|
||||
@@ -37,13 +37,13 @@ export function isChatMessageSchema(schema: JSONSchemaProperty | undefined): boo
|
||||
|
||||
const props = schema.properties;
|
||||
|
||||
// ChatMessage has "text" property (the main content)
|
||||
// Message has "text" property (the main content)
|
||||
const hasText = "text" in props && props.text?.type === "string";
|
||||
|
||||
// ChatMessage has "role" property (user, assistant, system)
|
||||
// Message has "role" property (user, assistant, system)
|
||||
const hasRole = "role" in props && props.role?.type === "string";
|
||||
|
||||
// If it has both text and role, it's likely a ChatMessage
|
||||
// If it has both text and role, it's likely a Message
|
||||
if (hasText && hasRole) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -17,16 +17,16 @@ from typing import Any, Generic
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from agent_framework import (
|
||||
Agent,
|
||||
AgentResponse,
|
||||
AgentResponseUpdate,
|
||||
AgentThread,
|
||||
BaseAgent,
|
||||
BaseChatClient,
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
Message,
|
||||
ResponseStream,
|
||||
)
|
||||
from agent_framework._clients import OptionsCoT
|
||||
@@ -67,17 +67,17 @@ class MockChatClient:
|
||||
|
||||
async def get_response(
|
||||
self,
|
||||
messages: str | ChatMessage | list[str] | list[ChatMessage],
|
||||
messages: str | Message | list[str] | list[Message],
|
||||
**kwargs: Any,
|
||||
) -> ChatResponse:
|
||||
self.call_count += 1
|
||||
if self.responses:
|
||||
return self.responses.pop(0)
|
||||
return ChatResponse(messages=ChatMessage("assistant", ["test response"]))
|
||||
return ChatResponse(messages=Message("assistant", ["test response"]))
|
||||
|
||||
async def get_streaming_response(
|
||||
self,
|
||||
messages: str | ChatMessage | list[str] | list[ChatMessage],
|
||||
messages: str | Message | list[str] | list[Message],
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[ChatResponseUpdate]:
|
||||
self.call_count += 1
|
||||
@@ -101,13 +101,13 @@ class MockBaseChatClient(BaseChatClient[OptionsCoT], Generic[OptionsCoT]):
|
||||
self.run_responses: list[ChatResponse] = []
|
||||
self.streaming_responses: list[list[ChatResponseUpdate]] = []
|
||||
self.call_count: int = 0
|
||||
self.received_messages: list[list[ChatMessage]] = []
|
||||
self.received_messages: list[list[Message]] = []
|
||||
|
||||
@override
|
||||
def _inner_get_response(
|
||||
self,
|
||||
*,
|
||||
messages: Sequence[ChatMessage],
|
||||
messages: Sequence[Message],
|
||||
stream: bool,
|
||||
options: Mapping[str, Any],
|
||||
**kwargs: Any,
|
||||
@@ -120,11 +120,11 @@ class MockBaseChatClient(BaseChatClient[OptionsCoT], Generic[OptionsCoT]):
|
||||
self.received_messages.append(list(messages))
|
||||
if self.run_responses:
|
||||
return self.run_responses.pop(0)
|
||||
return ChatResponse(messages=ChatMessage("assistant", ["Mock response from ChatAgent"]))
|
||||
return ChatResponse(messages=Message("assistant", ["Mock response from Agent"]))
|
||||
|
||||
return _get()
|
||||
|
||||
async def _stream_impl(self, messages: Sequence[ChatMessage]) -> AsyncIterable[ChatResponseUpdate]:
|
||||
async def _stream_impl(self, messages: Sequence[Message]) -> AsyncIterable[ChatResponseUpdate]:
|
||||
self.call_count += 1
|
||||
self.received_messages.append(list(messages))
|
||||
if self.streaming_responses:
|
||||
@@ -135,7 +135,7 @@ class MockBaseChatClient(BaseChatClient[OptionsCoT], Generic[OptionsCoT]):
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="Mock ")], role="assistant")
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="streaming ")], role="assistant")
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="response ")], role="assistant")
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="from ChatAgent")], role="assistant")
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="from Agent")], role="assistant")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -159,7 +159,7 @@ class MockAgent(BaseAgent):
|
||||
|
||||
def run(
|
||||
self,
|
||||
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
|
||||
messages: str | Message | list[str] | list[Message] | None = None,
|
||||
*,
|
||||
stream: bool = False,
|
||||
thread: AgentThread | None = None,
|
||||
@@ -172,17 +172,17 @@ class MockAgent(BaseAgent):
|
||||
|
||||
async def _run(
|
||||
self,
|
||||
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
|
||||
messages: str | Message | list[str] | list[Message] | None = None,
|
||||
*,
|
||||
thread: AgentThread | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AgentResponse:
|
||||
self.call_count += 1
|
||||
return AgentResponse(messages=[ChatMessage("assistant", [Content.from_text(text=self.response_text)])])
|
||||
return AgentResponse(messages=[Message("assistant", [Content.from_text(text=self.response_text)])])
|
||||
|
||||
def _run_stream(
|
||||
self,
|
||||
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
|
||||
messages: str | Message | list[str] | list[Message] | None = None,
|
||||
*,
|
||||
thread: AgentThread | None = None,
|
||||
**kwargs: Any,
|
||||
@@ -205,7 +205,7 @@ class MockToolCallingAgent(BaseAgent):
|
||||
|
||||
def run(
|
||||
self,
|
||||
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
|
||||
messages: str | Message | list[str] | list[Message] | None = None,
|
||||
*,
|
||||
stream: bool = False,
|
||||
thread: AgentThread | None = None,
|
||||
@@ -218,16 +218,16 @@ class MockToolCallingAgent(BaseAgent):
|
||||
|
||||
async def _run(
|
||||
self,
|
||||
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
|
||||
messages: str | Message | list[str] | list[Message] | None = None,
|
||||
*,
|
||||
thread: AgentThread | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AgentResponse:
|
||||
return AgentResponse(messages=[ChatMessage("assistant", ["done"])])
|
||||
return AgentResponse(messages=[Message("assistant", ["done"])])
|
||||
|
||||
def _run_stream(
|
||||
self,
|
||||
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
|
||||
messages: str | Message | list[str] | list[Message] | None = None,
|
||||
*,
|
||||
thread: AgentThread | None = None,
|
||||
**kwargs: Any,
|
||||
@@ -275,7 +275,7 @@ class MockToolCallingAgent(BaseAgent):
|
||||
|
||||
def _create_agent_run_response(text: str = "Test response") -> AgentResponse:
|
||||
"""Create an AgentResponse with the given text."""
|
||||
return AgentResponse(messages=[ChatMessage("assistant", [Content.from_text(text=text)])])
|
||||
return AgentResponse(messages=[Message("assistant", [Content.from_text(text=text)])])
|
||||
|
||||
|
||||
def _create_agent_executor_response(
|
||||
@@ -289,8 +289,8 @@ def _create_agent_executor_response(
|
||||
executor_id=executor_id,
|
||||
agent_response=agent_response,
|
||||
full_conversation=[
|
||||
ChatMessage("user", [Content.from_text(text="User input")]),
|
||||
ChatMessage("assistant", [Content.from_text(text=response_text)]),
|
||||
Message("user", [Content.from_text(text="User input")]),
|
||||
Message("assistant", [Content.from_text(text=response_text)]),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -318,7 +318,7 @@ def create_executor_completed_event(
|
||||
|
||||
This creates the exact data structure that caused the serialization bug:
|
||||
WorkflowEvent.data contains AgentExecutorResponse which contains
|
||||
AgentResponse and ChatMessage objects (SerializationMixin, not Pydantic).
|
||||
AgentResponse and Message objects (SerializationMixin, not Pydantic).
|
||||
"""
|
||||
data = _create_agent_executor_response(executor_id) if with_agent_response else {"simple": "dict"}
|
||||
return WorkflowEvent.executor_completed(executor_id=executor_id, data=data)
|
||||
@@ -390,7 +390,7 @@ def executor_completed_event() -> WorkflowEvent[Any]:
|
||||
|
||||
This creates the exact data structure that caused the serialization bug:
|
||||
executor_completed event (type='executor_completed').data contains AgentExecutorResponse which contains
|
||||
AgentResponse and ChatMessage objects (SerializationMixin, not Pydantic).
|
||||
AgentResponse and Message objects (SerializationMixin, not Pydantic).
|
||||
"""
|
||||
data = _create_agent_executor_response("test_executor")
|
||||
return WorkflowEvent.executor_completed(executor_id="test_executor", data=data)
|
||||
@@ -425,10 +425,10 @@ def test_entities_dir() -> str:
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def executor_with_real_agent() -> tuple[AgentFrameworkExecutor, str, MockBaseChatClient]:
|
||||
"""Create an executor with a REAL ChatAgent using mock chat client.
|
||||
"""Create an executor with a REAL Agent using mock chat client.
|
||||
|
||||
This tests the full execution pipeline:
|
||||
- Real ChatAgent class
|
||||
- Real Agent class
|
||||
- Real message handling and normalization
|
||||
- Real middleware pipeline
|
||||
- Only the LLM call is mocked
|
||||
@@ -440,12 +440,12 @@ async def executor_with_real_agent() -> tuple[AgentFrameworkExecutor, str, MockB
|
||||
mapper = MessageMapper()
|
||||
executor = AgentFrameworkExecutor(discovery, mapper)
|
||||
|
||||
# Create a REAL ChatAgent with mock client
|
||||
agent = ChatAgent(
|
||||
# Create a REAL Agent with mock client
|
||||
agent = Agent(
|
||||
id="test_chat_agent",
|
||||
name="Test Chat Agent",
|
||||
description="A real ChatAgent for testing execution flow",
|
||||
chat_client=mock_client,
|
||||
description="A real Agent for testing execution flow",
|
||||
client=mock_client,
|
||||
system_message="You are a helpful test assistant.",
|
||||
)
|
||||
|
||||
@@ -469,22 +469,22 @@ async def sequential_workflow() -> tuple[AgentFrameworkExecutor, str, MockBaseCh
|
||||
"""
|
||||
mock_client = MockBaseChatClient()
|
||||
mock_client.run_responses = [
|
||||
ChatResponse(messages=ChatMessage("assistant", ["Here's the draft content about the topic."])),
|
||||
ChatResponse(messages=ChatMessage("assistant", ["Review: Content is clear and well-structured."])),
|
||||
ChatResponse(messages=Message("assistant", ["Here's the draft content about the topic."])),
|
||||
ChatResponse(messages=Message("assistant", ["Review: Content is clear and well-structured."])),
|
||||
]
|
||||
|
||||
writer = ChatAgent(
|
||||
writer = Agent(
|
||||
id="writer",
|
||||
name="Writer",
|
||||
description="Content writer agent",
|
||||
chat_client=mock_client,
|
||||
client=mock_client,
|
||||
system_message="You are a content writer. Create clear, engaging content.",
|
||||
)
|
||||
reviewer = ChatAgent(
|
||||
reviewer = Agent(
|
||||
id="reviewer",
|
||||
name="Reviewer",
|
||||
description="Content reviewer agent",
|
||||
chat_client=mock_client,
|
||||
client=mock_client,
|
||||
system_message="You are a reviewer. Provide constructive feedback.",
|
||||
)
|
||||
|
||||
@@ -513,30 +513,30 @@ async def concurrent_workflow() -> tuple[AgentFrameworkExecutor, str, MockBaseCh
|
||||
"""
|
||||
mock_client = MockBaseChatClient()
|
||||
mock_client.run_responses = [
|
||||
ChatResponse(messages=ChatMessage("assistant", ["Research findings: Key data points identified."])),
|
||||
ChatResponse(messages=ChatMessage("assistant", ["Analysis: Trends indicate positive growth."])),
|
||||
ChatResponse(messages=ChatMessage("assistant", ["Summary: Overall outlook is favorable."])),
|
||||
ChatResponse(messages=Message("assistant", ["Research findings: Key data points identified."])),
|
||||
ChatResponse(messages=Message("assistant", ["Analysis: Trends indicate positive growth."])),
|
||||
ChatResponse(messages=Message("assistant", ["Summary: Overall outlook is favorable."])),
|
||||
]
|
||||
|
||||
researcher = ChatAgent(
|
||||
researcher = Agent(
|
||||
id="researcher",
|
||||
name="Researcher",
|
||||
description="Research agent",
|
||||
chat_client=mock_client,
|
||||
client=mock_client,
|
||||
system_message="You are a researcher. Find key data and insights.",
|
||||
)
|
||||
analyst = ChatAgent(
|
||||
analyst = Agent(
|
||||
id="analyst",
|
||||
name="Analyst",
|
||||
description="Analysis agent",
|
||||
chat_client=mock_client,
|
||||
client=mock_client,
|
||||
system_message="You are an analyst. Identify trends and patterns.",
|
||||
)
|
||||
summarizer = ChatAgent(
|
||||
summarizer = Agent(
|
||||
id="summarizer",
|
||||
name="Summarizer",
|
||||
description="Summary agent",
|
||||
chat_client=mock_client,
|
||||
client=mock_client,
|
||||
system_message="You are a summarizer. Provide concise summaries.",
|
||||
)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from agent_framework import AgentResponse, ChatMessage, Content
|
||||
from agent_framework import AgentResponse, Content, Message
|
||||
|
||||
from agent_framework_devui import register_cleanup
|
||||
from agent_framework_devui._discovery import EntityDiscovery
|
||||
@@ -39,12 +39,12 @@ class MockAgent:
|
||||
|
||||
async def _stream():
|
||||
yield AgentResponse(
|
||||
messages=[ChatMessage(role="assistant", contents=[Content.from_text(text="Test response")])],
|
||||
messages=[Message(role="assistant", contents=[Content.from_text(text="Test response")])],
|
||||
)
|
||||
|
||||
return _stream()
|
||||
return AgentResponse(
|
||||
messages=[ChatMessage(role="assistant", contents=[Content.from_text(text="Test response")])],
|
||||
messages=[Message(role="assistant", contents=[Content.from_text(text="Test response")])],
|
||||
)
|
||||
|
||||
|
||||
@@ -267,7 +267,7 @@ async def test_cleanup_with_file_based_discovery():
|
||||
# Write agent module with cleanup registration
|
||||
agent_file = agent_dir / "__init__.py"
|
||||
agent_file.write_text("""
|
||||
from agent_framework import AgentResponse, ChatMessage, Role, Content
|
||||
from agent_framework import AgentResponse, Message, Role, Content
|
||||
from agent_framework_devui import register_cleanup
|
||||
|
||||
class MockCredential:
|
||||
@@ -289,12 +289,12 @@ class TestAgent:
|
||||
if stream:
|
||||
async def _stream():
|
||||
yield AgentResponse(
|
||||
messages=[ChatMessage(role="assistant", content=[Content.from_text(text="Test")])],
|
||||
messages=[Message(role="assistant", content=[Content.from_text(text="Test")])],
|
||||
inner_messages=[],
|
||||
)
|
||||
return _stream()
|
||||
return AgentResponse(
|
||||
messages=[ChatMessage(role="assistant", content=[Content.from_text(text="Test")])],
|
||||
messages=[Message(role="assistant", content=[Content.from_text(text="Test")])],
|
||||
inner_messages=[],
|
||||
)
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ async def test_list_items_pagination():
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_items_converts_function_calls():
|
||||
"""Test that list_items properly converts function calls to ResponseFunctionToolCallItem."""
|
||||
from agent_framework import ChatMessage, ChatMessageStore
|
||||
from agent_framework import ChatMessageStore, Message
|
||||
|
||||
store = InMemoryConversationStore()
|
||||
|
||||
@@ -216,8 +216,8 @@ async def test_list_items_converts_function_calls():
|
||||
|
||||
# Simulate messages from agent execution with function calls
|
||||
messages = [
|
||||
ChatMessage(role="user", contents=[{"type": "text", "text": "What's the weather in SF?"}]),
|
||||
ChatMessage(
|
||||
Message(role="user", contents=[{"type": "text", "text": "What's the weather in SF?"}]),
|
||||
Message(
|
||||
role="assistant",
|
||||
contents=[
|
||||
{
|
||||
@@ -228,7 +228,7 @@ async def test_list_items_converts_function_calls():
|
||||
}
|
||||
],
|
||||
),
|
||||
ChatMessage(
|
||||
Message(
|
||||
role="tool",
|
||||
contents=[
|
||||
{
|
||||
@@ -238,7 +238,7 @@ async def test_list_items_converts_function_calls():
|
||||
}
|
||||
],
|
||||
),
|
||||
ChatMessage(role="assistant", contents=[{"type": "text", "text": "The weather is sunny, 65°F"}]),
|
||||
Message(role="assistant", contents=[{"type": "text", "text": "The weather is sunny, 65°F"}]),
|
||||
]
|
||||
|
||||
# Add messages to thread
|
||||
@@ -284,7 +284,7 @@ async def test_list_items_converts_function_calls():
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_items_handles_images_and_files():
|
||||
"""Test that list_items properly converts data content (images/files) to OpenAI types."""
|
||||
from agent_framework import ChatMessage, ChatMessageStore
|
||||
from agent_framework import ChatMessageStore, Message
|
||||
|
||||
store = InMemoryConversationStore()
|
||||
|
||||
@@ -300,7 +300,7 @@ async def test_list_items_handles_images_and_files():
|
||||
|
||||
# Simulate message with image and file
|
||||
messages = [
|
||||
ChatMessage(
|
||||
Message(
|
||||
role="user",
|
||||
contents=[
|
||||
{"type": "text", "text": "Check this image and PDF"},
|
||||
|
||||
@@ -74,7 +74,7 @@ async def test_discovery_accepts_agents_with_only_run():
|
||||
|
||||
init_file = agent_dir / "__init__.py"
|
||||
init_file.write_text("""
|
||||
from agent_framework import AgentResponse, AgentThread, ChatMessage, Role, Content
|
||||
from agent_framework import AgentResponse, AgentThread, Message, Role, Content
|
||||
|
||||
class NonStreamingAgent:
|
||||
id = "non_streaming"
|
||||
@@ -83,7 +83,7 @@ class NonStreamingAgent:
|
||||
|
||||
async def run(self, messages=None, *, thread=None, **kwargs):
|
||||
return AgentResponse(
|
||||
messages=[ChatMessage(
|
||||
messages=[Message(
|
||||
role="assistant",
|
||||
contents=[Content.from_text(text="response")]
|
||||
)],
|
||||
@@ -188,14 +188,14 @@ workflow = WorkflowBuilder(start_executor=executor).build()
|
||||
agent_dir = temp_path / "my_agent"
|
||||
agent_dir.mkdir()
|
||||
(agent_dir / "agent.py").write_text("""
|
||||
from agent_framework import AgentResponse, AgentThread, ChatMessage, Role, TextContent
|
||||
from agent_framework import AgentResponse, AgentThread, Message, Role, TextContent
|
||||
|
||||
class TestAgent:
|
||||
name = "Test Agent"
|
||||
|
||||
async def run(self, messages=None, *, thread=None, **kwargs):
|
||||
return AgentResponse(
|
||||
messages=[ChatMessage(role="assistant", contents=[Content.from_text(text="test")])],
|
||||
messages=[Message(role="assistant", contents=[Content.from_text(text="test")])],
|
||||
response_id="test"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Tests include:
|
||||
- Entity discovery and info retrieval
|
||||
- Agent execution (sync and streaming) using real ChatAgent with mock LLM
|
||||
- Agent execution (sync and streaming) using real Agent with mock LLM
|
||||
- Workflow execution using real WorkflowBuilder with FunctionExecutor
|
||||
- Edge cases like non-streaming agents
|
||||
"""
|
||||
@@ -15,7 +15,7 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from agent_framework import AgentExecutor, ChatAgent, FunctionExecutor, WorkflowBuilder
|
||||
from agent_framework import Agent, AgentExecutor, FunctionExecutor, WorkflowBuilder
|
||||
|
||||
# Import mock classes from conftest for direct use in some tests
|
||||
from conftest import MockBaseChatClient
|
||||
@@ -77,15 +77,15 @@ async def test_executor_get_entity_info(executor):
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Agent Execution Tests (using real ChatAgent with mock LLM)
|
||||
# Agent Execution Tests (using real Agent with mock LLM)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
async def test_agent_sync_execution(executor_with_real_agent):
|
||||
"""Test synchronous agent execution with REAL ChatAgent (mock LLM).
|
||||
"""Test synchronous agent execution with REAL Agent (mock LLM).
|
||||
|
||||
This tests the full execution pipeline without needing an API key:
|
||||
- Real ChatAgent class with middleware
|
||||
- Real Agent class with middleware
|
||||
- Real message normalization
|
||||
- Mock chat client for LLM calls
|
||||
"""
|
||||
@@ -130,7 +130,7 @@ async def test_agent_sync_execution_respects_model_field(executor_with_real_agen
|
||||
async def test_chat_client_receives_correct_messages(executor_with_real_agent):
|
||||
"""Verify the mock chat client receives properly formatted messages.
|
||||
|
||||
This tests that the REAL ChatAgent properly:
|
||||
This tests that the REAL Agent properly:
|
||||
- Normalizes input messages
|
||||
- Formats messages for the chat client
|
||||
"""
|
||||
@@ -297,18 +297,18 @@ async def test_full_pipeline_workflow_events_are_json_serializable():
|
||||
|
||||
This is particularly important for workflows with AgentExecutor because:
|
||||
- AgentExecutor produces executor_completed event (type='executor_completed') with AgentExecutorResponse
|
||||
- AgentExecutorResponse contains AgentResponse and ChatMessage objects
|
||||
- AgentExecutorResponse contains AgentResponse and Message objects
|
||||
- These are SerializationMixin objects, not Pydantic, which caused the original bug
|
||||
|
||||
This test ensures the ENTIRE streaming pipeline works end-to-end.
|
||||
"""
|
||||
# Create a workflow with AgentExecutor (the problematic case)
|
||||
mock_client = MockBaseChatClient()
|
||||
agent = ChatAgent(
|
||||
agent = Agent(
|
||||
id="serialization_test_agent",
|
||||
name="Serialization Test Agent",
|
||||
description="Agent for testing serialization",
|
||||
chat_client=mock_client,
|
||||
client=mock_client,
|
||||
system_message="You are a test assistant.",
|
||||
)
|
||||
|
||||
@@ -466,15 +466,15 @@ async def test_executor_parse_raw_string_for_string_workflow():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_executor_parse_converts_to_chat_message_for_sequential_workflow(sequential_workflow):
|
||||
"""Sequential workflows convert string input to ChatMessage."""
|
||||
from agent_framework import ChatMessage
|
||||
"""Sequential workflows convert string input to Message."""
|
||||
from agent_framework import Message
|
||||
|
||||
executor, _entity_id, _mock_client, workflow = sequential_workflow
|
||||
|
||||
# Sequential workflows expect ChatMessage, so raw string becomes ChatMessage
|
||||
# Sequential workflows expect Message, so raw string becomes Message
|
||||
parsed = executor._parse_raw_workflow_input(workflow, "hello")
|
||||
|
||||
assert isinstance(parsed, ChatMessage)
|
||||
assert isinstance(parsed, Message)
|
||||
assert parsed.text == "hello"
|
||||
|
||||
|
||||
@@ -538,7 +538,7 @@ def test_extract_workflow_hil_responses_handles_stringified_json():
|
||||
|
||||
async def test_executor_handles_streaming_agent():
|
||||
"""Test executor handles agents with run(stream=True) method."""
|
||||
from agent_framework import AgentResponse, AgentResponseUpdate, AgentThread, ChatMessage, Content
|
||||
from agent_framework import AgentResponse, AgentResponseUpdate, AgentThread, Content, Message
|
||||
|
||||
class StreamingAgent:
|
||||
"""Agent with run() method supporting stream parameter."""
|
||||
@@ -556,7 +556,7 @@ async def test_executor_handles_streaming_agent():
|
||||
|
||||
async def _run_impl(self, messages):
|
||||
return AgentResponse(
|
||||
messages=[ChatMessage(role="assistant", contents=[Content.from_text(text=f"Processed: {messages}")])],
|
||||
messages=[Message(role="assistant", contents=[Content.from_text(text=f"Processed: {messages}")])],
|
||||
response_id="test_123",
|
||||
)
|
||||
|
||||
|
||||
@@ -304,7 +304,7 @@ async def test_executor_completed_event_with_agent_response(
|
||||
|
||||
This is a REGRESSION TEST for the serialization bug where
|
||||
WorkflowEvent.data contained AgentExecutorResponse with nested
|
||||
AgentResponse and ChatMessage objects (SerializationMixin) that
|
||||
AgentResponse and Message objects (SerializationMixin) that
|
||||
Pydantic couldn't serialize.
|
||||
"""
|
||||
# Create event with realistic nested data - the exact structure that caused the bug
|
||||
@@ -579,13 +579,13 @@ async def test_workflow_output_event(mapper: MessageMapper, test_request: AgentF
|
||||
|
||||
async def test_workflow_output_event_with_list_data(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test output event (type='output') with list data (common for sequential/concurrent workflows)."""
|
||||
from agent_framework import ChatMessage
|
||||
from agent_framework import Message
|
||||
from agent_framework._workflows._events import WorkflowEvent
|
||||
|
||||
# Sequential/Concurrent workflows often output list[ChatMessage]
|
||||
# Sequential/Concurrent workflows often output list[Message]
|
||||
messages = [
|
||||
ChatMessage(role="user", contents=[Content.from_text(text="Hello")]),
|
||||
ChatMessage(role="assistant", contents=[Content.from_text(text="World")]),
|
||||
Message(role="user", contents=[Content.from_text(text="Hello")]),
|
||||
Message(role="assistant", contents=[Content.from_text(text="World")]),
|
||||
]
|
||||
event = WorkflowEvent.output(executor_id="complete", data=messages)
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
@@ -48,8 +48,8 @@ class TestMultimodalWorkflowInput:
|
||||
assert executor._is_openai_multimodal_format([{"foo": "bar"}]) is False # no type field
|
||||
|
||||
def test_convert_openai_input_to_chat_message_with_image(self):
|
||||
"""Test that OpenAI format with image is converted to ChatMessage with DataContent."""
|
||||
from agent_framework import ChatMessage
|
||||
"""Test that OpenAI format with image is converted to Message with DataContent."""
|
||||
from agent_framework import Message
|
||||
|
||||
discovery = MagicMock(spec=EntityDiscovery)
|
||||
mapper = MagicMock(spec=MessageMapper)
|
||||
@@ -67,11 +67,11 @@ class TestMultimodalWorkflowInput:
|
||||
}
|
||||
]
|
||||
|
||||
# Convert to ChatMessage
|
||||
# Convert to Message
|
||||
result = executor._convert_input_to_chat_message(openai_input)
|
||||
|
||||
# Verify result is ChatMessage
|
||||
assert isinstance(result, ChatMessage), f"Expected ChatMessage, got {type(result)}"
|
||||
# Verify result is Message
|
||||
assert isinstance(result, Message), f"Expected Message, got {type(result)}"
|
||||
assert result.role == "user"
|
||||
|
||||
# Verify contents
|
||||
@@ -89,7 +89,7 @@ class TestMultimodalWorkflowInput:
|
||||
async def test_parse_workflow_input_handles_json_string_with_multimodal(self):
|
||||
"""Test that _parse_workflow_input correctly handles JSON string with multimodal content."""
|
||||
|
||||
from agent_framework import ChatMessage
|
||||
from agent_framework import Message
|
||||
|
||||
discovery = MagicMock(spec=EntityDiscovery)
|
||||
mapper = MagicMock(spec=MessageMapper)
|
||||
@@ -114,8 +114,8 @@ class TestMultimodalWorkflowInput:
|
||||
# Parse the input
|
||||
result = await executor._parse_workflow_input(mock_workflow, json_string_input)
|
||||
|
||||
# Verify result is ChatMessage with multimodal content
|
||||
assert isinstance(result, ChatMessage), f"Expected ChatMessage, got {type(result)}"
|
||||
# Verify result is Message with multimodal content
|
||||
assert isinstance(result, Message), f"Expected Message, got {type(result)}"
|
||||
assert len(result.contents) == 2
|
||||
|
||||
# Verify text content
|
||||
@@ -129,7 +129,7 @@ class TestMultimodalWorkflowInput:
|
||||
async def test_parse_workflow_input_still_handles_simple_dict(self):
|
||||
"""Test that simple dict input still works (backward compatibility)."""
|
||||
|
||||
from agent_framework import ChatMessage
|
||||
from agent_framework import Message
|
||||
|
||||
discovery = MagicMock(spec=EntityDiscovery)
|
||||
mapper = MagicMock(spec=MessageMapper)
|
||||
@@ -139,14 +139,14 @@ class TestMultimodalWorkflowInput:
|
||||
simple_input = {"text": "Hello world", "role": "user"}
|
||||
json_string_input = json.dumps(simple_input)
|
||||
|
||||
# Mock workflow with ChatMessage input type
|
||||
# Mock workflow with Message input type
|
||||
mock_workflow = MagicMock()
|
||||
mock_executor = MagicMock()
|
||||
mock_executor.input_types = [ChatMessage]
|
||||
mock_executor.input_types = [Message]
|
||||
mock_workflow.get_start_executor.return_value = mock_executor
|
||||
|
||||
# Parse the input
|
||||
result = await executor._parse_workflow_input(mock_workflow, json_string_input)
|
||||
|
||||
# Result should be ChatMessage (from _parse_structured_workflow_input)
|
||||
assert isinstance(result, ChatMessage), f"Expected ChatMessage, got {type(result)}"
|
||||
# Result should be Message (from _parse_structured_workflow_input)
|
||||
assert isinstance(result, Message), f"Expected Message, got {type(result)}"
|
||||
|
||||
@@ -67,16 +67,16 @@ def test_dataclass_schema_generation():
|
||||
|
||||
|
||||
def test_chat_message_schema_generation():
|
||||
"""Test schema generation for ChatMessage (SerializationMixin)."""
|
||||
"""Test schema generation for Message (SerializationMixin)."""
|
||||
try:
|
||||
from agent_framework import ChatMessage
|
||||
from agent_framework import Message
|
||||
|
||||
schema = generate_input_schema(ChatMessage)
|
||||
schema = generate_input_schema(Message)
|
||||
assert schema is not None
|
||||
assert isinstance(schema, dict)
|
||||
|
||||
except ImportError:
|
||||
pytest.skip("ChatMessage not available - agent_framework not installed")
|
||||
pytest.skip("Message not available - agent_framework not installed")
|
||||
|
||||
|
||||
def test_pydantic_model_schema_generation():
|
||||
|
||||
@@ -142,7 +142,7 @@ async def test_credential_cleanup() -> None:
|
||||
"""Test that async credentials are properly closed during server cleanup."""
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
from agent_framework import ChatAgent
|
||||
from agent_framework import Agent
|
||||
|
||||
# Create mock credential with async close
|
||||
mock_credential = AsyncMock()
|
||||
@@ -155,7 +155,7 @@ async def test_credential_cleanup() -> None:
|
||||
mock_client.function_invocation_configuration = None
|
||||
|
||||
# Create agent with mock client
|
||||
agent = ChatAgent(name="TestAgent", chat_client=mock_client, instructions="Test agent")
|
||||
agent = Agent(name="TestAgent", client=mock_client, instructions="Test agent")
|
||||
|
||||
# Create DevUI server with agent
|
||||
server = DevServer()
|
||||
@@ -175,7 +175,7 @@ async def test_credential_cleanup_error_handling() -> None:
|
||||
"""Test that credential cleanup errors are handled gracefully."""
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
from agent_framework import ChatAgent
|
||||
from agent_framework import Agent
|
||||
|
||||
# Create mock credential that raises error on close
|
||||
mock_credential = AsyncMock()
|
||||
@@ -188,7 +188,7 @@ async def test_credential_cleanup_error_handling() -> None:
|
||||
mock_client.function_invocation_configuration = None
|
||||
|
||||
# Create agent with mock client
|
||||
agent = ChatAgent(name="TestAgent", chat_client=mock_client, instructions="Test agent")
|
||||
agent = Agent(name="TestAgent", client=mock_client, instructions="Test agent")
|
||||
|
||||
# Create DevUI server with agent
|
||||
server = DevServer()
|
||||
@@ -207,7 +207,7 @@ async def test_multiple_credential_attributes() -> None:
|
||||
"""Test that we check all common credential attribute names."""
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
from agent_framework import ChatAgent
|
||||
from agent_framework import Agent
|
||||
|
||||
# Create mock credentials
|
||||
mock_cred1 = Mock()
|
||||
@@ -223,7 +223,7 @@ async def test_multiple_credential_attributes() -> None:
|
||||
mock_client.function_invocation_configuration = None
|
||||
|
||||
# Create agent with mock client
|
||||
agent = ChatAgent(name="TestAgent", chat_client=mock_client, instructions="Test agent")
|
||||
agent = Agent(name="TestAgent", client=mock_client, instructions="Test agent")
|
||||
|
||||
# Create DevUI server with agent
|
||||
server = DevServer()
|
||||
|
||||
Reference in New Issue
Block a user