Skip to content

Python (DevUI): Serialization Error for Handoff Workflow Outputs in DevUI #1920

@q33566

Description

@q33566

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)

Metadata

Metadata

Labels

devuiDevUI-related itemspythonworkflowsRelated to Workflows in agent-framework

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions