-
Notifications
You must be signed in to change notification settings - Fork 687
Description
Problem
When using Handoff Workflows that yield list[ChatMessage] as output, DevUI throws a serialization error:
[2025-11-05 09:43:19 - C:\Kurt\03_agent_framework\multi-agent-sdk.venv\Lib\site-packages\agent_framework_devui\_server.py:544 - ERROR] Error in streaming execution: Unable to serialize unknown type: <class 'agent_framework._types.ChatMessage'>
The error occurs in agent_framework_devui/_server.py at line 544 when trying to serialize WorkflowOutputEvent data containing list[ChatMessage].
Root Cause
The serialization logic in agent_framework_devui/_mapper.py only checks if the event data itself has a to_dict() method. When WorkflowOutputEvent.data is a list[ChatMessage], the list object doesn't have to_dict(), so the ChatMessage objects inside remain unserialized, causing JSON serialization to fail.
Expected Behavior
Handoff Workflows should be able to yield list[ChatMessage] outputs without serialization errors. The mapper should recursively serialize nested SerializationMixin objects in lists and other data structures.
Actual Behavior
Serialization error occurs when workflow completes and tries to emit the output event.
Environment
- agent-framework=1.0.0b251104
Reproduce
Run the script, and type your query in devUI
import asyncio
from operator import truediv
import os
from typing import Annotated
from pydantic import Field
from dotenv import load_dotenv
load_dotenv()
from azure.identity import DefaultAzureCredential
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework import (
HandoffBuilder,
WorkflowEvent,
WorkflowOutputEvent,
ChatAgent,
)
import logging
import sys
logging.basicConfig(
level=logging.DEBUG,
format='[%(asctime)s - %(name)s - %(filename)s:%(lineno)d - %(levelname)s] %(message)s',
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler('debug.log', mode='w', encoding='utf-8')
]
)
# 設置特定模組的日誌等級
logging.getLogger('agent_framework').setLevel(logging.DEBUG)
logging.getLogger('agent_framework_devui').setLevel(logging.DEBUG)
logging.getLogger('agent_framework._workflows').setLevel(logging.DEBUG)
# ---- Minimal mock tools for specialists ----
def mk_travel_plan(location: Annotated[str, Field(description="Location")]) -> str:
print(f"mk_travel_plan tool executed")
return f"[TravelPlanner] Plan for {location}: Day 1/2/3..."
def mk_route(location: Annotated[str, Field(description="Location")]) -> str:
print(f"mk_route tool executed")
return f"[RouteMaster] Best route adjustments for {location}."
def mk_weather(location: Annotated[str, Field(description="Location")]) -> str:
print(f"mk_weather tool executed")
return f"[WeatherAdvisor] Forecast summary for {location}: sunny."
def mk_hotel(location: Annotated[str, Field(description="Location")]) -> str:
print(f"mk_hotel tool executed")
return f"[HotelFinder] Recommended hotel in {location}: The Test Hotel."
def mk_budget() -> str:
print(f"mk_budget tool executed")
return "[TripBudget] Estimated total cost: 1234 TWD."
def mk_html() -> str:
print(f"mk_html tool executed")
return "[TravelStoryHTMLnImage] HTML generated with 1 image and map embeds."
async def main() -> None:
chat_client = AzureOpenAIChatClient(
credential=DefaultAzureCredential(),
project_endpoint=os.environ["AIPROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_OPENAI_DEPLOYMENT"],
)
# ---- Create agents directly with client (no Team helper) ----
coordinator: ChatAgent = chat_client.create_agent(
name="TaskPlanner",
description="Coordinator that always delegates using handoff tools.",
instructions=(
"You are the coordinator. Read the latest user message and ALWAYS delegate the next step "
"by calling exactly one handoff tool to the most suitable specialist. "
"Specialists available: TravelPlanner, WeatherAdvisor, HotelFinder, RouteMaster, TripBudget, TravelStoryHTMLnImage. "
"Examples of tools you may call: handoff_to_TravelPlanner, handoff_to_WeatherAdvisor, handoff_to_HotelFinder, "
"handoff_to_RouteMaster, handoff_to_TripBudget, handoff_to_TravelStoryHTMLnImage. "
"Do not produce final results yourself."
),
)
travel_planner: ChatAgent = chat_client.create_agent(
name="TravelPlanner",
description="Creates a simple itinerary.",
instructions="Reply with a concise itinerary using your tool.",
# tools=[mk_travel_plan],
)
route_master: ChatAgent = chat_client.create_agent(
name="RouteMaster",
description="Optimizes routes.",
instructions="Reply with route improvements using your tool.",
tools=[mk_route],
)
weather_advisor: ChatAgent = chat_client.create_agent(
name="WeatherAdvisor",
description="Provides a short forecast.",
instructions="Reply with a forecast using your tool.",
tools=[mk_weather],
)
hotel_finder: ChatAgent = chat_client.create_agent(
name="HotelFinder",
description="Finds one suitable hotel.",
instructions="Reply with one hotel using your tool.",
tools=[mk_hotel],
)
trip_budget: ChatAgent = chat_client.create_agent(
name="TripBudget",
description="Outputs a simple budget number.",
instructions="Reply with a budget using your tool.",
tools=[mk_budget],
)
finalizer: ChatAgent = chat_client.create_agent(
name="TravelStoryHTMLnImage",
description="Generates a simple HTML summary.",
instructions="Reply by generating HTML using your tool.",
tools=[mk_html],
)
# ---- Build handoff workflow ----
def terminate_when_finalizer(conversation) -> bool:
return any(m.author_name == travel_planner.name for m in reversed(conversation))
workflow = (
HandoffBuilder(
name="minimal_handoff",
participants=[
coordinator,
travel_planner,
# route_master,
# weather_advisor,
# hotel_finder,
# trip_budget,
# finalizer,
],
)
.set_coordinator(coordinator)
.add_handoff(coordinator, travel_planner, tool_description="Delegate to TravelPlanner for initial itinerary.")
# .add_handoff(coordinator, travel_planner, tool_description="Delegate to TravelPlanner for initial itinerary.")
# .add_handoff(coordinator, weather_advisor, tool_description="Delegate to WeatherAdvisor for forecast adjustments.")
# .add_handoff(coordinator, hotel_finder, tool_description="Delegate to HotelFinder for one hotel suggestion.")
# .add_handoff(coordinator, route_master, tool_description="Delegate to RouteMaster for route optimization.")
# .add_handoff(coordinator, trip_budget, tool_description="Delegate to TripBudget for a quick budget.")
# .add_handoff(coordinator, finalizer, tool_description="Delegate to TravelStoryHTMLnImage to generate HTML.")
.with_termination_condition(
lambda conv: sum(1 for msg in conv if msg.role.value == "user") >= 5
or any("goodbye" in msg.text.lower() for msg in conv[-2:])
)
.build()
)
return workflow
if __name__ == "__main__":
# Setup workflow in async context
workflow = asyncio.run(main())
from agent_framework.devui import serve
serve(entities=[workflow], port=8093, auto_open=True, tracing_enabled=True)