Python: [Breaking] removed pydantic from types and workflows (#917)

* removed pydantic from types

* fix test

* fix test

* fix tests

* fix assistants client

* Remove Pydantic usage from workflow code.

* updated pydantic removal

* updated lock and test fixes

* fix mypy

* updated build system

* updated chat client parsing

* fix broken test

---------

Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
This commit is contained in:
Eduard van Valkenburg
2025-09-29 23:19:58 +02:00
committed by GitHub
Unverified
parent 647db9635a
commit b4ebafa9b1
56 changed files with 3881 additions and 1735 deletions
@@ -159,7 +159,6 @@ def test_concurrent_custom_aggregator_uses_callback_name_for_id() -> None:
assert aggregator.id == "summarize"
@pytest.mark.asyncio
async def test_concurrent_checkpoint_resume_round_trip() -> None:
storage = InMemoryCheckpointStorage()
@@ -44,8 +44,10 @@ class MockMessageSecondary:
class MockExecutor(Executor):
"""A mock executor for testing purposes."""
call_count: int = 0
last_message: Any = None
def __init__(self, *, id: str) -> None:
super().__init__(id=id)
self.call_count: int = 0
self.last_message: MockMessage | None = None
@handler
async def mock_handler(self, message: MockMessage, ctx: WorkflowContext) -> None:
@@ -57,8 +59,10 @@ class MockExecutor(Executor):
class MockExecutorSecondary(Executor):
"""A secondary mock executor for testing purposes."""
call_count: int = 0
last_message: Any = None
def __init__(self, *, id: str) -> None:
super().__init__(id=id)
self.call_count: int = 0
self.last_message: MockMessageSecondary | None = None
@handler
async def mock_handler_secondary(self, message: MockMessageSecondary, ctx: WorkflowContext) -> None:
@@ -70,8 +74,10 @@ class MockExecutorSecondary(Executor):
class MockAggregator(Executor):
"""A mock aggregator for testing purposes."""
call_count: int = 0
last_message: Any = None
def __init__(self, *, id: str) -> None:
super().__init__(id=id)
self.call_count: int = 0
self.last_message: list[MockMessage] | list[MockMessageSecondary] | None = None
@handler
async def mock_aggregator_handler(self, message: list[MockMessage], ctx: WorkflowContext) -> None:
@@ -93,8 +99,10 @@ class MockAggregator(Executor):
class MockAggregatorSecondary(Executor):
"""A mock aggregator that has a handler for a union type for testing purposes."""
call_count: int = 0
last_message: Any = None
def __init__(self, *, id: str) -> None:
super().__init__(id=id)
self.call_count: int = 0
self.last_message: list[MockMessage | MockMessageSecondary] | None = None
@handler
async def mock_aggregator_handler_combine(
@@ -9,6 +9,8 @@ import pytest
from agent_framework import (
AgentRunResponse,
AgentRunResponseUpdate,
BaseAgent,
ChatClientProtocol,
ChatMessage,
ChatResponse,
ChatResponseUpdate,
@@ -31,8 +33,6 @@ from agent_framework import (
WorkflowStatusEvent,
handler,
)
from agent_framework._agents import BaseAgent
from agent_framework._clients import ChatClientProtocol as AFChatClient
from agent_framework._workflow._checkpoint import InMemoryCheckpointStorage
from agent_framework._workflow._magentic import (
MagenticAgentExecutor,
@@ -105,8 +105,8 @@ class FakeManager(MagenticManagerBase):
if self.task_ledger is not None:
state = dict(state)
state["task_ledger"] = {
"facts": self.task_ledger.facts.model_dump(mode="json"),
"plan": self.task_ledger.plan.model_dump(mode="json"),
"facts": self.task_ledger.facts.to_dict(),
"plan": self.task_ledger.plan.to_dict(),
}
return state
@@ -118,8 +118,8 @@ class FakeManager(MagenticManagerBase):
plan_payload = ledger_state.get("plan") # type: ignore[reportUnknownMemberType]
if facts_payload is not None and plan_payload is not None:
try:
facts = ChatMessage.model_validate(facts_payload)
plan = ChatMessage.model_validate(plan_payload)
facts = ChatMessage.from_dict(facts_payload)
plan = ChatMessage.from_dict(plan_payload)
self.task_ledger = _SimpleLedger(facts=facts, plan=plan)
except Exception: # pragma: no cover - defensive
pass
@@ -159,11 +159,11 @@ async def test_standard_manager_plan_and_replan_combined_ledger():
participant_descriptions={"agentA": "Agent A"},
)
first = await manager.plan(ctx.model_copy(deep=True))
first = await manager.plan(ctx.clone())
assert first.role == Role.ASSISTANT and "Facts:" in first.text and "Plan:" in first.text
assert manager.task_ledger is not None
replanned = await manager.replan(ctx.model_copy(deep=True))
replanned = await manager.replan(ctx.clone())
assert "A2" in replanned.text or "Do Z" in replanned.text
@@ -174,12 +174,12 @@ async def test_standard_manager_progress_ledger_and_fallback():
participant_descriptions={"agentA": "Agent A"},
)
ledger = await manager.create_progress_ledger(ctx.model_copy(deep=True))
ledger = await manager.create_progress_ledger(ctx.clone())
assert isinstance(ledger, MagenticProgressLedger)
assert ledger.next_speaker.answer == "agentA"
manager.satisfied_after_signoff = False
ledger2 = await manager.create_progress_ledger(ctx.model_copy(deep=True))
ledger2 = await manager.create_progress_ledger(ctx.clone())
assert ledger2.is_request_satisfied.answer is False
@@ -379,7 +379,7 @@ def test_magentic_agent_executor_snapshot_roundtrip():
from agent_framework import StandardMagenticManager # noqa: E402
class _StubChatClient(AFChatClient):
class _StubChatClient(ChatClientProtocol):
@property
def additional_properties(self) -> dict[str, Any]:
"""Get additional properties associated with the client."""
@@ -412,7 +412,7 @@ async def test_standard_manager_plan_and_replan_via_complete_monkeypatch():
task=ChatMessage(role=Role.USER, text="T"),
participant_descriptions={"A": "desc"},
)
combined = await mgr.plan(ctx.model_copy(deep=True))
combined = await mgr.plan(ctx.clone())
# Assert structural headings and that steps appear in the combined ledger output.
assert "We are working to address the following user request:" in combined.text
assert "Here is the plan to follow as best as possible:" in combined.text
@@ -425,7 +425,7 @@ async def test_standard_manager_plan_and_replan_via_complete_monkeypatch():
return ChatMessage(role=Role.ASSISTANT, text="GIVEN OR VERIFIED FACTS\n- updated")
mgr._complete = fake_complete_replan # type: ignore[attr-defined]
combined2 = await mgr.replan(ctx.model_copy(deep=True))
combined2 = await mgr.replan(ctx.clone())
assert "updated" in combined2.text or "new step" in combined2.text
@@ -448,7 +448,7 @@ async def test_standard_manager_progress_ledger_success_and_error():
return ChatMessage(role=Role.ASSISTANT, text=json_text)
mgr._complete = fake_complete_ok # type: ignore[attr-defined]
ledger = await mgr.create_progress_ledger(ctx.model_copy(deep=True))
ledger = await mgr.create_progress_ledger(ctx.clone())
assert ledger.next_speaker.answer == "alice"
# Error path: invalid JSON now raises to avoid emitting planner-oriented instructions to agents
@@ -457,7 +457,7 @@ async def test_standard_manager_progress_ledger_success_and_error():
mgr._complete = fake_complete_bad # type: ignore[attr-defined]
with pytest.raises(RuntimeError):
await mgr.create_progress_ledger(ctx.model_copy(deep=True))
await mgr.create_progress_ledger(ctx.clone())
class InvokeOnceManager(MagenticManagerBase):
@@ -48,8 +48,8 @@ class TestSerializationWorkflowClasses:
"""Test that Executor can be serialized and has correct fields, including type."""
executor = SampleExecutor(id="test-executor")
# Test model_dump
data = executor.model_dump(by_alias=True)
# Test to_dict
data = executor.to_dict()
assert data["id"] == "test-executor"
# Test type field
@@ -57,7 +57,7 @@ class TestSerializationWorkflowClasses:
assert data["type"] == "SampleExecutor", f"Expected type 'SampleExecutor', got {data['type']}"
# Test model_dump_json
json_str = executor.model_dump_json(by_alias=True)
json_str = executor.to_json()
parsed = json.loads(json_str)
assert parsed["id"] == "test-executor"
@@ -70,14 +70,14 @@ class TestSerializationWorkflowClasses:
# Test edge without condition
edge = Edge(source_id="source", target_id="target")
# Test model_dump
data = edge.model_dump()
# Test to_dict
data = edge.to_dict()
assert data["source_id"] == "source"
assert data["target_id"] == "target"
assert "condition_name" not in data or data["condition_name"] is None
# Test model_dump_json
json_str = edge.model_dump_json()
json_str = json.dumps(edge.to_dict())
parsed = json.loads(json_str)
assert parsed["source_id"] == "source"
assert parsed["target_id"] == "target"
@@ -91,14 +91,14 @@ class TestSerializationWorkflowClasses:
edge = Edge(source_id="source", target_id="target", condition=is_positive)
# Test model_dump
data = edge.model_dump()
# Test to_dict
data = edge.to_dict()
assert data["source_id"] == "source"
assert data["target_id"] == "target"
assert data["condition_name"] == "is_positive"
# Test model_dump_json
json_str = edge.model_dump_json()
json_str = json.dumps(edge.to_dict())
parsed = json.loads(json_str)
assert parsed["source_id"] == "source"
assert parsed["target_id"] == "target"
@@ -108,14 +108,14 @@ class TestSerializationWorkflowClasses:
"""Test that Edge with lambda condition serializes condition_name as '<lambda>'."""
edge = Edge(source_id="source", target_id="target", condition=lambda x: x > 0)
# Test model_dump
data = edge.model_dump()
# Test to_dict
data = edge.to_dict()
assert data["source_id"] == "source"
assert data["target_id"] == "target"
assert data["condition_name"] == "<lambda>"
# Test model_dump_json
json_str = edge.model_dump_json()
json_str = json.dumps(edge.to_dict())
parsed = json.loads(json_str)
assert parsed["source_id"] == "source"
assert parsed["target_id"] == "target"
@@ -125,8 +125,8 @@ class TestSerializationWorkflowClasses:
"""Test that SingleEdgeGroup can be serialized and has correct fields, including edges and type."""
edge_group = SingleEdgeGroup(source_id="source", target_id="target")
# Test model_dump
data = edge_group.model_dump(by_alias=True)
# Test to_dict
data = edge_group.to_dict()
assert "id" in data
assert data["id"].startswith("SingleEdgeGroup/")
@@ -144,7 +144,7 @@ class TestSerializationWorkflowClasses:
assert edge["target_id"] == "target", f"Expected target_id 'target', got {edge['target_id']}"
# Test model_dump_json
json_str = edge_group.model_dump_json()
json_str = json.dumps(edge_group.to_dict())
parsed = json.loads(json_str)
assert "id" in parsed
assert parsed["id"].startswith("SingleEdgeGroup/")
@@ -164,8 +164,8 @@ class TestSerializationWorkflowClasses:
"""Test that FanOutEdgeGroup can be serialized and has correct fields, including edges and type."""
edge_group = FanOutEdgeGroup(source_id="source", target_ids=["target1", "target2"])
# Test model_dump
data = edge_group.model_dump()
# Test to_dict
data = edge_group.to_dict()
assert "id" in data
assert data["id"].startswith("FanOutEdgeGroup/")
@@ -191,7 +191,7 @@ class TestSerializationWorkflowClasses:
assert set(targets) == {"target1", "target2"}, f"Expected targets {{'target1', 'target2'}}, got {set(targets)}"
# Test model_dump_json
json_str = edge_group.model_dump_json()
json_str = json.dumps(edge_group.to_dict())
parsed = json.loads(json_str)
assert "id" in parsed
assert parsed["id"].startswith("FanOutEdgeGroup/")
@@ -227,15 +227,15 @@ class TestSerializationWorkflowClasses:
source_id="source", target_ids=["target1", "target2"], selection_func=custom_selector
)
# Test model_dump
data = edge_group.model_dump()
# Test to_dict
data = edge_group.to_dict()
assert "selection_func_name" in data, "FanOutEdgeGroup should have 'selection_func_name' field"
assert data["selection_func_name"] == "custom_selector", (
f"Expected selection_func_name 'custom_selector', got {data['selection_func_name']}"
)
# Test model_dump_json
json_str = edge_group.model_dump_json()
json_str = json.dumps(edge_group.to_dict())
parsed = json.loads(json_str)
assert "selection_func_name" in parsed, "JSON should have 'selection_func_name' field"
assert parsed["selection_func_name"] == "custom_selector", "JSON should preserve selection_func_name"
@@ -246,15 +246,15 @@ class TestSerializationWorkflowClasses:
source_id="source", target_ids=["target1", "target2"], selection_func=lambda data, targets: targets[:1]
)
# Test model_dump
data = edge_group.model_dump()
# Test to_dict
data = edge_group.to_dict()
assert "selection_func_name" in data, "FanOutEdgeGroup should have 'selection_func_name' field"
assert data["selection_func_name"] == "<lambda>", (
f"Expected selection_func_name '<lambda>', got {data['selection_func_name']}"
)
# Test model_dump_json
json_str = edge_group.model_dump_json()
json_str = json.dumps(edge_group.to_dict())
parsed = json.loads(json_str)
assert "selection_func_name" in parsed, "JSON should have 'selection_func_name' field"
assert parsed["selection_func_name"] == "<lambda>", "JSON should preserve selection_func_name as '<lambda>'"
@@ -263,8 +263,8 @@ class TestSerializationWorkflowClasses:
"""Test that FanInEdgeGroup can be serialized and has correct fields, including edges and type."""
edge_group = FanInEdgeGroup(source_ids=["source1", "source2"], target_id="target")
# Test model_dump
data = edge_group.model_dump()
# Test to_dict
data = edge_group.to_dict()
assert "id" in data
assert data["id"].startswith("FanInEdgeGroup/")
@@ -284,7 +284,7 @@ class TestSerializationWorkflowClasses:
assert all(target == "target" for target in targets), f"All edges should have target 'target', got {targets}"
# Test model_dump_json
json_str = edge_group.model_dump_json()
json_str = json.dumps(edge_group.to_dict())
parsed = json.loads(json_str)
assert "id" in parsed
assert parsed["id"].startswith("FanInEdgeGroup/")
@@ -311,8 +311,8 @@ class TestSerializationWorkflowClasses:
]
edge_group = SwitchCaseEdgeGroup(source_id="source", cases=cases)
# Test model_dump
data = edge_group.model_dump()
# Test to_dict
data = edge_group.to_dict()
assert "id" in data
assert data["id"].startswith("SwitchCaseEdgeGroup/")
@@ -364,7 +364,7 @@ class TestSerializationWorkflowClasses:
)
# Test model_dump_json
json_str = edge_group.model_dump_json()
json_str = json.dumps(edge_group.to_dict())
parsed = json.loads(json_str)
assert "id" in parsed
assert parsed["id"].startswith("SwitchCaseEdgeGroup/")
@@ -436,7 +436,7 @@ class TestSerializationWorkflowClasses:
)
# Test serialization of the nested structure
data = outer_workflow.model_dump(by_alias=True)
data = outer_workflow.to_dict()
# Verify outer structure
assert data["start_executor_id"] == "outer-exec"
@@ -475,7 +475,7 @@ class TestSerializationWorkflowClasses:
assert "inner-exec" in innermost_workflow_data["executors"]
# Test JSON serialization preserves the complete nested structure
json_str = outer_workflow.model_dump_json(by_alias=True)
json_str = outer_workflow.to_json()
parsed = json.loads(json_str)
# Verify the complete structure is preserved in JSON
@@ -501,7 +501,7 @@ class TestSerializationWorkflowClasses:
assert "inner-exec" in innermost_workflow_json["executors"]
# Test that WorkflowExecutor also serializes correctly when accessed directly
direct_middle_data = middle_workflow_executor.model_dump(by_alias=True)
direct_middle_data = middle_workflow_executor.to_dict()
assert "workflow" in direct_middle_data
assert direct_middle_data["type"] == "WorkflowExecutor"
assert "executors" in direct_middle_data["workflow"]
@@ -519,8 +519,8 @@ class TestSerializationWorkflowClasses:
]
edge_group = SwitchCaseEdgeGroup(source_id="source", cases=cases)
# Test model_dump
data = edge_group.model_dump()
# Test to_dict
data = edge_group.to_dict()
assert "cases" in data, "SwitchCaseEdgeGroup should have 'cases' field"
cases_data = data["cases"]
@@ -530,7 +530,7 @@ class TestSerializationWorkflowClasses:
)
# Test model_dump_json
json_str = edge_group.model_dump_json()
json_str = json.dumps(edge_group.to_dict())
parsed = json.loads(json_str)
json_cases = parsed["cases"]
json_case_obj = json_cases[0]
@@ -544,7 +544,7 @@ class TestSerializationWorkflowClasses:
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
# Test model_dump
data = workflow.model_dump()
data = workflow.to_dict()
assert "edge_groups" in data
assert "executors" in data
assert "start_executor_id" in data
@@ -569,7 +569,7 @@ class TestSerializationWorkflowClasses:
assert edge["target_id"] == "executor2", f"Expected target_id 'executor2', got {edge['target_id']}"
# Test model_dump_json
json_str = workflow.model_dump_json()
json_str = workflow.to_json()
parsed = json.loads(json_str)
assert parsed["start_executor_id"] == "executor1"
assert "executor1" in parsed["executors"]
@@ -592,7 +592,7 @@ class TestSerializationWorkflowClasses:
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
# Test model_dump - should not include private runtime objects
data = workflow.model_dump()
data = workflow.to_dict()
# These private runtime fields should not be in the serialized data
assert "_runner_context" not in data
@@ -616,13 +616,11 @@ class TestSerializationWorkflowClasses:
assert edge.target_id == "target"
# Test validation failure for empty source_id
from pydantic import ValidationError
with pytest.raises(ValidationError):
with pytest.raises(ValueError):
Edge(source_id="", target_id="target")
# Test validation failure for empty target_id
with pytest.raises(ValidationError):
with pytest.raises(ValueError):
Edge(source_id="source", target_id="")
@@ -660,7 +658,7 @@ def test_comprehensive_edge_groups_workflow_serialization() -> None:
)
# Test workflow serialization
data = workflow.model_dump()
data = workflow.to_dict()
# Verify basic workflow structure
assert "edge_groups" in data
@@ -683,7 +681,7 @@ def test_comprehensive_edge_groups_workflow_serialization() -> None:
assert "SingleEdgeGroup" in edge_group_types, f"Expected SingleEdgeGroup in {edge_group_types}"
# Test JSON serialization
json_str = workflow.model_dump_json()
json_str = workflow.to_json()
parsed = json.loads(json_str)
# Verify JSON structure matches model_dump
@@ -3,7 +3,6 @@
from dataclasses import dataclass
from typing import Any
from pydantic import Field
from typing_extensions import Never
from agent_framework import (
@@ -62,13 +61,10 @@ def create_email_validation_workflow() -> Workflow:
class BasicParent(Executor):
"""Basic parent executor for simple sub-workflow tests."""
result: ValidationResult | None = Field(default=None)
cache: dict[str, bool] = Field(default_factory=dict)
def __init__(self, cache: dict[str, bool] | None = None, **kwargs: Any):
if cache is not None:
kwargs["cache"] = cache
super().__init__(id="basic_parent", **kwargs)
def __init__(self, cache: dict[str, bool] | None = None) -> None:
super().__init__(id="basic_parent")
self.result: ValidationResult | None = None
self.cache: dict[str, bool] = dict(cache) if cache is not None else {}
@handler
async def start(self, email: str, ctx: WorkflowContext[EmailValidationRequest]) -> None:
@@ -140,13 +136,12 @@ class EmailValidator(Executor):
class ParentOrchestrator(Executor):
"""Parent workflow orchestrator with domain knowledge."""
approved_domains: set[str] = Field(default_factory=lambda: {"example.com", "test.org"})
results: list[ValidationResult] = Field(default_factory=list)
def __init__(self, approved_domains: set[str] | None = None, **kwargs: Any):
if approved_domains is not None:
kwargs["approved_domains"] = approved_domains
super().__init__(id="parent_orchestrator", **kwargs)
def __init__(self, approved_domains: set[str] | None = None) -> None:
super().__init__(id="parent_orchestrator")
self.approved_domains: set[str] = (
set(approved_domains) if approved_domains is not None else {"example.com", "test.org"}
)
self.results: list[ValidationResult] = []
@handler
async def start(self, emails: list[str], ctx: WorkflowContext[EmailValidationRequest]) -> None:
@@ -278,10 +273,9 @@ async def test_workflow_scoped_interception() -> None:
class MultiWorkflowParent(Executor):
"""Parent handling multiple sub-workflows."""
results: dict[str, ValidationResult] = Field(default_factory=dict)
def __init__(self, **kwargs: Any):
super().__init__(id="multi_parent", **kwargs)
def __init__(self) -> None:
super().__init__(id="multi_parent")
self.results: dict[str, ValidationResult] = {}
@handler
async def start(self, data: dict[str, str], ctx: WorkflowContext[EmailValidationRequest]) -> None:
@@ -362,10 +356,9 @@ async def test_concurrent_sub_workflow_execution() -> None:
class ConcurrentProcessor(Executor):
"""Processor that sends multiple concurrent requests to the same sub-workflow."""
results: list[ValidationResult] = Field(default_factory=list)
def __init__(self, **kwargs: Any):
super().__init__(id="concurrent_processor", **kwargs)
def __init__(self) -> None:
super().__init__(id="concurrent_processor")
self.results: list[ValidationResult] = []
@handler
async def start(self, emails: list[str], ctx: WorkflowContext[EmailValidationRequest]) -> None:
@@ -35,8 +35,10 @@ class NumberMessage:
class IncrementExecutor(Executor):
"""An executor that increments message data by a specified amount for testing purposes."""
limit: int = 10
increment: int = 1
def __init__(self, id: str, *, limit: int = 10, increment: int = 1) -> None:
super().__init__(id=id)
self.limit = limit
self.increment = increment
@handler
async def mock_handler(self, message: NumberMessage, ctx: WorkflowContext[NumberMessage, int]) -> None:
@@ -29,11 +29,10 @@ from agent_framework import (
class SimpleExecutor(Executor):
"""Simple executor that emits AgentRunEvent or AgentRunStreamingEvent."""
response_text: str
emit_streaming: bool = False
def __init__(self, id: str, response_text: str, emit_streaming: bool = False):
super().__init__(id=id, response_text=response_text, emit_streaming=emit_streaming)
super().__init__(id=id)
self.response_text = response_text
self.emit_streaming = emit_streaming
@handler
async def handle_message(self, message: list[ChatMessage], ctx: WorkflowContext[list[ChatMessage]]) -> None:
@@ -273,7 +273,7 @@ async def test_end_to_end_workflow_tracing(span_exporter: InMemorySpanExporter)
assert build_span.attributes.get(OtelAttr.WORKFLOW_ID) == workflow.id
assert build_span.attributes.get("workflow.definition") is not None
definition = build_span.attributes.get("workflow.definition")
assert definition == workflow.model_dump_json(by_alias=True)
assert definition == workflow.to_json()
# Check build events
assert build_span.events is not None