Python: [Breaking] Simplified Content types to a single class with classmethod constructors. (#3252)

* ported Content to a new model

* fixed linting

* fixes

* fixed data format handling

* fix for 3.10 mypy

* fix

* fix int test
This commit is contained in:
Eduard van Valkenburg
2026-01-20 23:09:39 +01:00
committed by GitHub
Unverified
parent 73761aa4a3
commit 83e6229c11
132 changed files with 3949 additions and 4741 deletions
@@ -7,7 +7,7 @@ import tempfile
from pathlib import Path
import pytest
from agent_framework import AgentResponse, ChatMessage, Role, TextContent
from agent_framework import AgentResponse, ChatMessage, Content, Role
from agent_framework_devui import register_cleanup
from agent_framework_devui._discovery import EntityDiscovery
@@ -36,7 +36,7 @@ class MockAgent:
async def run_stream(self, messages=None, *, thread=None, **kwargs):
"""Mock streaming run method."""
yield AgentResponse(
messages=[ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text="Test response")])],
messages=[ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="Test response")])],
)
@@ -259,7 +259,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, TextContent
from agent_framework import AgentResponse, ChatMessage, Role, Content
from agent_framework_devui import register_cleanup
class MockCredential:
@@ -279,7 +279,7 @@ class TestAgent:
async def run_stream(self, messages=None, *, thread=None, **kwargs):
yield AgentResponse(
messages=[ChatMessage(role=Role.ASSISTANT, content=[TextContent(text="Test")])],
messages=[ChatMessage(role=Role.ASSISTANT, content=[Content.from_text(text="Test")])],
inner_messages=[],
)
@@ -84,7 +84,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, TextContent
from agent_framework import AgentResponse, AgentThread, ChatMessage, Role, Content
class NonStreamingAgent:
id = "non_streaming"
@@ -95,7 +95,7 @@ class NonStreamingAgent:
return AgentResponse(
messages=[ChatMessage(
role=Role.ASSISTANT,
contents=[TextContent(text="response")]
contents=[Content.from_text(text="response")]
)],
response_id="test"
)
@@ -210,7 +210,7 @@ class TestAgent:
async def run(self, messages=None, *, thread=None, **kwargs):
return AgentResponse(
messages=[ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text="test")])],
messages=[ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="test")])],
response_id="test"
)
@@ -566,7 +566,7 @@ def test_extract_workflow_hil_responses_handles_stringified_json():
async def test_executor_handles_non_streaming_agent():
"""Test executor can handle agents with only run() method (no run_stream)."""
from agent_framework import AgentResponse, AgentThread, ChatMessage, Role, TextContent
from agent_framework import AgentResponse, AgentThread, ChatMessage, Content, Role
class NonStreamingAgent:
"""Agent with only run() method - does NOT satisfy full AgentProtocol."""
@@ -577,7 +577,9 @@ async def test_executor_handles_non_streaming_agent():
async def run(self, messages=None, *, thread=None, **kwargs):
return AgentResponse(
messages=[ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text=f"Processed: {messages}")])],
messages=[
ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text=f"Processed: {messages}")])
],
response_id="test_123",
)
+15 -17
View File
@@ -28,11 +28,9 @@ from agent_framework import (
ChatResponse,
ChatResponseUpdate,
ConcurrentBuilder,
FunctionCallContent,
FunctionResultContent,
Content,
Role,
SequentialBuilder,
TextContent,
use_chat_middleware,
)
from agent_framework._clients import TOptions_co
@@ -93,7 +91,7 @@ class MockChatClient:
for update in self.streaming_responses.pop(0):
yield update
else:
yield ChatResponseUpdate(text=TextContent(text="test streaming response"), role="assistant")
yield ChatResponseUpdate(text=Content.from_text(text="test streaming response"), role="assistant")
@use_chat_middleware
@@ -141,10 +139,10 @@ class MockBaseChatClient(BaseChatClient[TOptions_co], Generic[TOptions_co]):
yield update
else:
# Simulate realistic streaming chunks
yield ChatResponseUpdate(text=TextContent(text="Mock "), role="assistant")
yield ChatResponseUpdate(text=TextContent(text="streaming "), role="assistant")
yield ChatResponseUpdate(text=TextContent(text="response "), role="assistant")
yield ChatResponseUpdate(text=TextContent(text="from ChatAgent"), role="assistant")
yield ChatResponseUpdate(text=Content.from_text(text="Mock "), role="assistant")
yield ChatResponseUpdate(text=Content.from_text(text="streaming "), role="assistant")
yield ChatResponseUpdate(text=Content.from_text(text="response "), role="assistant")
yield ChatResponseUpdate(text=Content.from_text(text="from ChatAgent"), role="assistant")
# =============================================================================
@@ -175,7 +173,7 @@ class MockAgent(BaseAgent):
) -> AgentResponse:
self.call_count += 1
return AgentResponse(
messages=[ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text=self.response_text)])]
messages=[ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text=self.response_text)])]
)
async def run_stream(
@@ -187,7 +185,7 @@ class MockAgent(BaseAgent):
) -> AsyncIterable[AgentResponseUpdate]:
self.call_count += 1
for chunk in self.streaming_chunks:
yield AgentResponseUpdate(contents=[TextContent(text=chunk)], role=Role.ASSISTANT)
yield AgentResponseUpdate(contents=[Content.from_text(text=chunk)], role=Role.ASSISTANT)
class MockToolCallingAgent(BaseAgent):
@@ -217,13 +215,13 @@ class MockToolCallingAgent(BaseAgent):
self.call_count += 1
# First: text
yield AgentResponseUpdate(
contents=[TextContent(text="Let me search for that...")],
contents=[Content.from_text(text="Let me search for that...")],
role=Role.ASSISTANT,
)
# Second: tool call
yield AgentResponseUpdate(
contents=[
FunctionCallContent(
Content.from_function_call(
call_id="call_123",
name="search",
arguments={"query": "weather"},
@@ -234,7 +232,7 @@ class MockToolCallingAgent(BaseAgent):
# Third: tool result
yield AgentResponseUpdate(
contents=[
FunctionResultContent(
Content.from_function_result(
call_id="call_123",
result={"temperature": 72, "condition": "sunny"},
)
@@ -243,7 +241,7 @@ class MockToolCallingAgent(BaseAgent):
)
# Fourth: final text
yield AgentResponseUpdate(
contents=[TextContent(text="The weather is sunny, 72°F.")],
contents=[Content.from_text(text="The weather is sunny, 72°F.")],
role=Role.ASSISTANT,
)
@@ -297,7 +295,7 @@ def create_mock_tool_agent(id: str = "tool_agent", name: str = "ToolAgent") -> M
def create_agent_run_response(text: str = "Test response") -> AgentResponse:
"""Create an AgentResponse with the given text."""
return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text=text)])])
return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text=text)])])
def create_agent_executor_response(
@@ -310,8 +308,8 @@ def create_agent_executor_response(
executor_id=executor_id,
agent_response=agent_response,
full_conversation=[
ChatMessage(role=Role.USER, contents=[TextContent(text="User input")]),
ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text=response_text)]),
ChatMessage(role=Role.USER, contents=[Content.from_text(text="User input")]),
ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text=response_text)]),
],
)
+19 -20
View File
@@ -14,11 +14,8 @@ import pytest
# Import Agent Framework types
from agent_framework._types import (
AgentResponseUpdate,
ErrorContent,
FunctionCallContent,
FunctionResultContent,
Content,
Role,
TextContent,
)
# Import real workflow event classes - NOT mocks!
@@ -71,15 +68,17 @@ def test_request() -> AgentFrameworkRequest:
def create_test_content(content_type: str, **kwargs: Any) -> Any:
"""Create test content objects."""
if content_type == "text":
return TextContent(text=kwargs.get("text", "Hello, world!"))
return Content.from_text(text=kwargs.get("text", "Hello, world!"))
if content_type == "function_call":
return FunctionCallContent(
return Content.from_function_call(
call_id=kwargs.get("call_id", "test_call_id"),
name=kwargs.get("name", "test_func"),
arguments=kwargs.get("arguments", {"param": "value"}),
)
if content_type == "error":
return ErrorContent(message=kwargs.get("message", "Test error"), error_code=kwargs.get("code", "test_error"))
return Content.from_error(
message=kwargs.get("message", "Test error"), error_code=kwargs.get("code", "test_error")
)
raise ValueError(f"Unknown content type: {content_type}")
@@ -162,7 +161,7 @@ async def test_function_result_content_with_string_result(
mapper: MessageMapper, test_request: AgentFrameworkRequest
) -> None:
"""Test FunctionResultContent with plain string result (regular tools)."""
content = FunctionResultContent(
content = Content.from_function_result(
call_id="test_call_123",
result="Hello, World!",
)
@@ -182,9 +181,9 @@ async def test_function_result_content_with_nested_content_objects(
mapper: MessageMapper, test_request: AgentFrameworkRequest
) -> None:
"""Test FunctionResultContent with nested Content objects (MCP tools case)."""
content = FunctionResultContent(
content = Content.from_function_result(
call_id="mcp_call_456",
result=[TextContent(text="Hello from MCP!")],
result=[Content.from_text(text="Hello from MCP!")],
)
update = create_test_agent_update([content])
@@ -451,12 +450,12 @@ async def test_magentic_agent_run_update_event_with_agent_delta_metadata(
This tests the ACTUAL event format Magentic emits - not a fake MagenticAgentDeltaEvent class.
Magentic uses AgentRunUpdateEvent with additional_properties containing magentic_event_type.
"""
from agent_framework._types import AgentResponseUpdate, Role, TextContent
from agent_framework._types import AgentResponseUpdate, Role
from agent_framework._workflows._events import AgentRunUpdateEvent
# Create the REAL event format that Magentic emits
update = AgentResponseUpdate(
contents=[TextContent(text="Hello from agent")],
contents=[Content.from_text(text="Hello from agent")],
role=Role.ASSISTANT,
author_name="Writer",
additional_properties={
@@ -482,12 +481,12 @@ async def test_magentic_orchestrator_message_event(mapper: MessageMapper, test_r
Magentic emits orchestrator planning/instruction messages using AgentRunUpdateEvent
with additional_properties containing magentic_event_type='orchestrator_message'.
"""
from agent_framework._types import AgentResponseUpdate, Role, TextContent
from agent_framework._types import AgentResponseUpdate, Role
from agent_framework._workflows._events import AgentRunUpdateEvent
# Create orchestrator message event (REAL format from Magentic)
update = AgentResponseUpdate(
contents=[TextContent(text="Planning: First, the writer will create content...")],
contents=[Content.from_text(text="Planning: First, the writer will create content...")],
role=Role.ASSISTANT,
author_name="Orchestrator",
additional_properties={
@@ -518,20 +517,20 @@ async def test_magentic_events_use_same_event_class_as_other_workflows(
additional_properties. Any mapper code checking for 'MagenticAgentDeltaEvent'
class names is dead code.
"""
from agent_framework._types import AgentResponseUpdate, Role, TextContent
from agent_framework._types import AgentResponseUpdate, Role
from agent_framework._workflows._events import AgentRunUpdateEvent
# Create events the way different workflows do it
# 1. Regular workflow (no additional_properties)
regular_update = AgentResponseUpdate(
contents=[TextContent(text="Regular workflow response")],
contents=[Content.from_text(text="Regular workflow response")],
role=Role.ASSISTANT,
)
regular_event = AgentRunUpdateEvent(executor_id="regular_executor", data=regular_update)
# 2. Magentic workflow (with additional_properties)
magentic_update = AgentResponseUpdate(
contents=[TextContent(text="Magentic workflow response")],
contents=[Content.from_text(text="Magentic workflow response")],
role=Role.ASSISTANT,
additional_properties={"magentic_event_type": "agent_delta"},
)
@@ -599,13 +598,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 WorkflowOutputEvent with list data (common for sequential/concurrent workflows)."""
from agent_framework import ChatMessage, Role, TextContent
from agent_framework import ChatMessage, Role
from agent_framework._workflows._events import WorkflowOutputEvent
# Sequential/Concurrent workflows often output list[ChatMessage]
messages = [
ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")]),
ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text="World")]),
ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")]),
ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="World")]),
]
event = WorkflowOutputEvent(data=messages, executor_id="complete")
events = await mapper.convert_event(event, test_request)
@@ -49,7 +49,7 @@ class TestMultimodalWorkflowInput:
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, DataContent, Role, TextContent
from agent_framework import ChatMessage, Role
discovery = MagicMock(spec=EntityDiscovery)
mapper = MagicMock(spec=MessageMapper)
@@ -78,11 +78,11 @@ class TestMultimodalWorkflowInput:
assert len(result.contents) == 2, f"Expected 2 contents, got {len(result.contents)}"
# First content should be text
assert isinstance(result.contents[0], TextContent)
assert result.contents[0].type == "text"
assert result.contents[0].text == "Describe this image"
# Second content should be image (DataContent)
assert isinstance(result.contents[1], DataContent)
assert result.contents[1].type == "data"
assert result.contents[1].media_type == "image/png"
assert result.contents[1].uri == TEST_IMAGE_DATA_URI
@@ -90,7 +90,7 @@ class TestMultimodalWorkflowInput:
"""Test that _parse_workflow_input correctly handles JSON string with multimodal content."""
import asyncio
from agent_framework import ChatMessage, DataContent, TextContent
from agent_framework import ChatMessage
discovery = MagicMock(spec=EntityDiscovery)
mapper = MagicMock(spec=MessageMapper)
@@ -120,11 +120,11 @@ class TestMultimodalWorkflowInput:
assert len(result.contents) == 2
# Verify text content
assert isinstance(result.contents[0], TextContent)
assert result.contents[0].type == "text"
assert result.contents[0].text == "What is in this image?"
# Verify image content
assert isinstance(result.contents[1], DataContent)
assert result.contents[1].type == "data"
assert result.contents[1].media_type == "image/png"
def test_parse_workflow_input_still_handles_simple_dict(self):