Python: Fix GroupChat orchestrator message cleanup issue (#3712)

* Fix GroupChat orchestrator message cleanup issue

Apply clean_conversation_for_handoff to GroupChatOrchestrator and
AgentBasedGroupChatOrchestrator _handle_response methods to remove
tool-related content that causes API errors from empty messages.

Fixes #3705

* Move orchestration related files to orchestrations package.

* Fix imports

---------

Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
This commit is contained in:
Ben Thomas
2026-02-05 19:50:37 -08:00
committed by GitHub
Unverified
parent f96772f6e8
commit c609b14f63
20 changed files with 131 additions and 174 deletions
@@ -7,12 +7,6 @@ from ._agent_executor import (
AgentExecutorResponse,
)
from ._agent_utils import resolve_agent_id
from ._base_group_chat_orchestrator import (
BaseGroupChatOrchestrator,
GroupChatRequestMessage,
GroupChatRequestSentEvent,
GroupChatResponseReceivedEvent,
)
from ._checkpoint import (
CheckpointStorage,
FileCheckpointStorage,
@@ -65,8 +59,6 @@ from ._executor import (
handler,
)
from ._function_executor import FunctionExecutor, executor
from ._orchestration_request_info import AgentRequestInfoResponse
from ._orchestration_state import OrchestrationState
from ._request_info_mixin import response_handler
from ._runner import Runner
from ._runner_context import (
@@ -97,8 +89,6 @@ __all__ = [
"AgentExecutor",
"AgentExecutorRequest",
"AgentExecutorResponse",
"AgentRequestInfoResponse",
"BaseGroupChatOrchestrator",
"Case",
"CheckpointStorage",
"Default",
@@ -115,13 +105,9 @@ __all__ = [
"FileCheckpointStorage",
"FunctionExecutor",
"GraphConnectivityError",
"GroupChatRequestMessage",
"GroupChatRequestSentEvent",
"GroupChatResponseReceivedEvent",
"InMemoryCheckpointStorage",
"InProcRunnerContext",
"Message",
"OrchestrationState",
"RequestInfoEvent",
"Runner",
"RunnerContext",
@@ -17,6 +17,17 @@ _IMPORTS = [
"HandoffBuilder",
"HandoffConfiguration",
"HandoffSentEvent",
# Base orchestrator
"BaseGroupChatOrchestrator",
"GroupChatRequestMessage",
"GroupChatRequestSentEvent",
"GroupChatResponseReceivedEvent",
"TerminationCondition",
# Orchestration helpers
"AgentRequestInfoResponse",
"OrchestrationState",
"clean_conversation_for_handoff",
"create_completion_message",
# Group Chat
"AgentBasedGroupChatOrchestrator",
"AgentOrchestrationOutput",
@@ -1,108 +1,39 @@
# Copyright (c) Microsoft. All rights reserved.
# Type stubs for lazy-loaded orchestrations module
# These re-export types from agent_framework_orchestrations
from agent_framework_orchestrations import (
# Magentic
MAGENTIC_MANAGER_NAME as MAGENTIC_MANAGER_NAME,
)
from agent_framework_orchestrations import (
ORCH_MSG_KIND_INSTRUCTION as ORCH_MSG_KIND_INSTRUCTION,
)
from agent_framework_orchestrations import (
ORCH_MSG_KIND_NOTICE as ORCH_MSG_KIND_NOTICE,
)
from agent_framework_orchestrations import (
ORCH_MSG_KIND_TASK_LEDGER as ORCH_MSG_KIND_TASK_LEDGER,
)
from agent_framework_orchestrations import (
ORCH_MSG_KIND_USER_TASK as ORCH_MSG_KIND_USER_TASK,
)
from agent_framework_orchestrations import (
# Group Chat
AgentBasedGroupChatOrchestrator as AgentBasedGroupChatOrchestrator,
)
from agent_framework_orchestrations import (
AgentOrchestrationOutput as AgentOrchestrationOutput,
)
from agent_framework_orchestrations import (
# Concurrent
ConcurrentBuilder as ConcurrentBuilder,
)
from agent_framework_orchestrations import (
GroupChatBuilder as GroupChatBuilder,
)
from agent_framework_orchestrations import (
GroupChatOrchestrator as GroupChatOrchestrator,
)
from agent_framework_orchestrations import (
GroupChatSelectionFunction as GroupChatSelectionFunction,
)
from agent_framework_orchestrations import (
GroupChatState as GroupChatState,
)
from agent_framework_orchestrations import (
# Handoff
HandoffAgentExecutor as HandoffAgentExecutor,
)
from agent_framework_orchestrations import (
HandoffAgentUserRequest as HandoffAgentUserRequest,
)
from agent_framework_orchestrations import (
HandoffBuilder as HandoffBuilder,
)
from agent_framework_orchestrations import (
HandoffConfiguration as HandoffConfiguration,
)
from agent_framework_orchestrations import (
HandoffSentEvent as HandoffSentEvent,
)
from agent_framework_orchestrations import (
MagenticAgentExecutor as MagenticAgentExecutor,
)
from agent_framework_orchestrations import (
MagenticBuilder as MagenticBuilder,
)
from agent_framework_orchestrations import (
MagenticContext as MagenticContext,
)
from agent_framework_orchestrations import (
MagenticManagerBase as MagenticManagerBase,
)
from agent_framework_orchestrations import (
MagenticOrchestrator as MagenticOrchestrator,
)
from agent_framework_orchestrations import (
MagenticOrchestratorEvent as MagenticOrchestratorEvent,
)
from agent_framework_orchestrations import (
MagenticOrchestratorEventType as MagenticOrchestratorEventType,
)
from agent_framework_orchestrations import (
MagenticPlanReviewRequest as MagenticPlanReviewRequest,
)
from agent_framework_orchestrations import (
MagenticPlanReviewResponse as MagenticPlanReviewResponse,
)
from agent_framework_orchestrations import (
MagenticProgressLedger as MagenticProgressLedger,
)
from agent_framework_orchestrations import (
MagenticProgressLedgerItem as MagenticProgressLedgerItem,
)
from agent_framework_orchestrations import (
MagenticResetSignal as MagenticResetSignal,
)
from agent_framework_orchestrations import (
# Sequential
SequentialBuilder as SequentialBuilder,
)
from agent_framework_orchestrations import (
StandardMagenticManager as StandardMagenticManager,
)
from agent_framework_orchestrations import (
__version__ as __version__,
MAGENTIC_MANAGER_NAME,
ORCH_MSG_KIND_INSTRUCTION,
ORCH_MSG_KIND_NOTICE,
ORCH_MSG_KIND_TASK_LEDGER,
ORCH_MSG_KIND_USER_TASK,
AgentBasedGroupChatOrchestrator,
AgentOrchestrationOutput,
AgentRequestInfoResponse,
ConcurrentBuilder,
GroupChatBuilder,
GroupChatOrchestrator,
GroupChatSelectionFunction,
GroupChatState,
HandoffAgentExecutor,
HandoffAgentUserRequest,
HandoffBuilder,
HandoffConfiguration,
HandoffSentEvent,
MagenticAgentExecutor,
MagenticBuilder,
MagenticContext,
MagenticManagerBase,
MagenticOrchestrator,
MagenticOrchestratorEvent,
MagenticOrchestratorEventType,
MagenticPlanReviewRequest,
MagenticPlanReviewResponse,
MagenticProgressLedger,
MagenticProgressLedgerItem,
MagenticResetSignal,
SequentialBuilder,
StandardMagenticManager,
__version__,
)
__all__ = [
@@ -113,6 +44,7 @@ __all__ = [
"ORCH_MSG_KIND_USER_TASK",
"AgentBasedGroupChatOrchestrator",
"AgentOrchestrationOutput",
"AgentRequestInfoResponse",
"ConcurrentBuilder",
"GroupChatBuilder",
"GroupChatOrchestrator",
@@ -17,6 +17,13 @@ try:
except importlib.metadata.PackageNotFoundError:
__version__ = "0.0.0" # Fallback for development mode
from ._base_group_chat_orchestrator import (
BaseGroupChatOrchestrator,
GroupChatRequestMessage,
GroupChatRequestSentEvent,
GroupChatResponseReceivedEvent,
TerminationCondition,
)
from ._concurrent import ConcurrentBuilder
from ._group_chat import (
AgentBasedGroupChatOrchestrator,
@@ -53,6 +60,9 @@ from ._magentic import (
MagenticResetSignal,
StandardMagenticManager,
)
from ._orchestration_request_info import AgentRequestInfoResponse
from ._orchestration_state import OrchestrationState
from ._orchestrator_helpers import clean_conversation_for_handoff, create_completion_message
from ._sequential import SequentialBuilder
__all__ = [
@@ -63,9 +73,14 @@ __all__ = [
"ORCH_MSG_KIND_USER_TASK",
"AgentBasedGroupChatOrchestrator",
"AgentOrchestrationOutput",
"AgentRequestInfoResponse",
"BaseGroupChatOrchestrator",
"ConcurrentBuilder",
"GroupChatBuilder",
"GroupChatOrchestrator",
"GroupChatRequestMessage",
"GroupChatRequestSentEvent",
"GroupChatResponseReceivedEvent",
"GroupChatSelectionFunction",
"GroupChatState",
"HandoffAgentExecutor",
@@ -85,7 +100,11 @@ __all__ = [
"MagenticProgressLedger",
"MagenticProgressLedgerItem",
"MagenticResetSignal",
"OrchestrationState",
"SequentialBuilder",
"StandardMagenticManager",
"TerminationCondition",
"__version__",
"clean_conversation_for_handoff",
"create_completion_message",
]
@@ -12,14 +12,14 @@ from collections.abc import Awaitable, Callable, Sequence
from dataclasses import dataclass
from typing import Any, ClassVar, TypeAlias
from agent_framework._types import ChatMessage
from agent_framework._workflows._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse
from agent_framework._workflows._events import WorkflowEvent
from agent_framework._workflows._executor import Executor, handler
from agent_framework._workflows._workflow_context import WorkflowContext
from typing_extensions import Never
from .._types import ChatMessage
from ._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse
from ._events import WorkflowEvent
from ._executor import Executor, handler
from ._orchestration_request_info import AgentApprovalExecutor
from ._workflow_context import WorkflowContext
if sys.version_info >= (3, 12):
from typing import override # type: ignore # pragma: no cover
@@ -12,12 +12,13 @@ from agent_framework._workflows._agent_utils import resolve_agent_id
from agent_framework._workflows._checkpoint import CheckpointStorage
from agent_framework._workflows._executor import Executor, handler
from agent_framework._workflows._message_utils import normalize_messages_input
from agent_framework._workflows._orchestration_request_info import AgentApprovalExecutor
from agent_framework._workflows._workflow import Workflow
from agent_framework._workflows._workflow_builder import WorkflowBuilder
from agent_framework._workflows._workflow_context import WorkflowContext
from typing_extensions import Never
from ._orchestration_request_info import AgentApprovalExecutor
logger = logging.getLogger(__name__)
"""Concurrent builder for agent-only fan-out/fan-in workflows.
@@ -481,7 +482,7 @@ class ConcurrentBuilder:
Returns:
Self for fluent chaining
"""
from agent_framework._workflows._orchestration_request_info import resolve_request_info_filter
from ._orchestration_request_info import resolve_request_info_filter
self._request_info_enabled = True
self._request_info_filter = resolve_request_info_filter(list(agents) if agents else None)
@@ -31,7 +31,16 @@ from agent_framework._threads import AgentThread
from agent_framework._types import ChatMessage
from agent_framework._workflows._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse
from agent_framework._workflows._agent_utils import resolve_agent_id
from agent_framework._workflows._base_group_chat_orchestrator import (
from agent_framework._workflows._checkpoint import CheckpointStorage
from agent_framework._workflows._conversation_state import decode_chat_messages, encode_chat_messages
from agent_framework._workflows._executor import Executor
from agent_framework._workflows._workflow import Workflow
from agent_framework._workflows._workflow_builder import WorkflowBuilder
from agent_framework._workflows._workflow_context import WorkflowContext
from pydantic import BaseModel, Field
from typing_extensions import Never
from ._base_group_chat_orchestrator import (
BaseGroupChatOrchestrator,
GroupChatParticipantMessage,
GroupChatRequestMessage,
@@ -40,15 +49,8 @@ from agent_framework._workflows._base_group_chat_orchestrator import (
ParticipantRegistry,
TerminationCondition,
)
from agent_framework._workflows._checkpoint import CheckpointStorage
from agent_framework._workflows._conversation_state import decode_chat_messages, encode_chat_messages
from agent_framework._workflows._executor import Executor
from agent_framework._workflows._orchestration_request_info import AgentApprovalExecutor
from agent_framework._workflows._workflow import Workflow
from agent_framework._workflows._workflow_builder import WorkflowBuilder
from agent_framework._workflows._workflow_context import WorkflowContext
from pydantic import BaseModel, Field
from typing_extensions import Never
from ._orchestration_request_info import AgentApprovalExecutor
from ._orchestrator_helpers import clean_conversation_for_handoff
if sys.version_info >= (3, 12):
from typing import override # type: ignore # pragma: no cover
@@ -192,6 +194,8 @@ class GroupChatOrchestrator(BaseGroupChatOrchestrator):
) -> None:
"""Handle a participant response."""
messages = self._process_participant_response(response)
# Remove tool-related content to prevent API errors from empty messages
messages = clean_conversation_for_handoff(messages)
self._append_messages(messages)
if await self._check_terminate_and_yield(cast(WorkflowContext[Never, list[ChatMessage]], ctx)):
@@ -359,6 +363,8 @@ class AgentBasedGroupChatOrchestrator(BaseGroupChatOrchestrator):
) -> None:
"""Handle a participant response."""
messages = self._process_participant_response(response)
# Remove tool-related content to prevent API errors from empty messages
messages = clean_conversation_for_handoff(messages)
self._append_messages(messages)
if await self._check_terminate_and_yield(cast(WorkflowContext[Never, list[ChatMessage]], ctx)):
return
@@ -877,7 +883,7 @@ class GroupChatBuilder:
Returns:
Self for fluent chaining
"""
from agent_framework._workflows._orchestration_request_info import resolve_request_info_filter
from ._orchestration_request_info import resolve_request_info_filter
self._request_info_enabled = True
self._request_info_filter = resolve_request_info_filter(list(agents) if agents else None)
@@ -43,16 +43,17 @@ from agent_framework._tools import FunctionTool, tool
from agent_framework._types import AgentResponse, AgentResponseUpdate, ChatMessage
from agent_framework._workflows._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse
from agent_framework._workflows._agent_utils import resolve_agent_id
from agent_framework._workflows._base_group_chat_orchestrator import TerminationCondition
from agent_framework._workflows._checkpoint import CheckpointStorage
from agent_framework._workflows._events import WorkflowEvent
from agent_framework._workflows._orchestrator_helpers import clean_conversation_for_handoff
from agent_framework._workflows._request_info_mixin import response_handler
from agent_framework._workflows._workflow import Workflow
from agent_framework._workflows._workflow_builder import WorkflowBuilder
from agent_framework._workflows._workflow_context import WorkflowContext
from typing_extensions import Never
from ._base_group_chat_orchestrator import TerminationCondition
from ._orchestrator_helpers import clean_conversation_for_handoff
if sys.version_info >= (3, 12):
from typing import override # type: ignore # pragma: no cover
else:
@@ -18,14 +18,6 @@ from agent_framework import (
ChatMessage,
)
from agent_framework._workflows._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse
from agent_framework._workflows._base_group_chat_orchestrator import (
BaseGroupChatOrchestrator,
GroupChatParticipantMessage,
GroupChatRequestMessage,
GroupChatResponseMessage,
GroupChatWorkflowContextOutT,
ParticipantRegistry,
)
from agent_framework._workflows._checkpoint import CheckpointStorage
from agent_framework._workflows._events import ExecutorEvent
from agent_framework._workflows._executor import Executor, handler
@@ -36,6 +28,15 @@ from agent_framework._workflows._workflow_builder import WorkflowBuilder
from agent_framework._workflows._workflow_context import WorkflowContext
from typing_extensions import Never
from ._base_group_chat_orchestrator import (
BaseGroupChatOrchestrator,
GroupChatParticipantMessage,
GroupChatRequestMessage,
GroupChatResponseMessage,
GroupChatWorkflowContextOutT,
ParticipantRegistry,
)
if sys.version_info >= (3, 12):
from typing import override # type: ignore # pragma: no cover
else:
@@ -2,16 +2,16 @@
from dataclasses import dataclass
from .._agents import AgentProtocol
from .._types import ChatMessage
from ._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse
from ._agent_utils import resolve_agent_id
from ._executor import Executor, handler
from ._request_info_mixin import response_handler
from ._workflow import Workflow
from ._workflow_builder import WorkflowBuilder
from ._workflow_context import WorkflowContext
from ._workflow_executor import WorkflowExecutor
from agent_framework._agents import AgentProtocol
from agent_framework._types import ChatMessage
from agent_framework._workflows._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse
from agent_framework._workflows._agent_utils import resolve_agent_id
from agent_framework._workflows._executor import Executor, handler
from agent_framework._workflows._request_info_mixin import response_handler
from agent_framework._workflows._workflow import Workflow
from agent_framework._workflows._workflow_builder import WorkflowBuilder
from agent_framework._workflows._workflow_context import WorkflowContext
from agent_framework._workflows._workflow_executor import WorkflowExecutor
def resolve_request_info_filter(agents: list[str | AgentProtocol] | None) -> set[str]:
@@ -9,7 +9,7 @@ across GroupChat, Handoff, and Magentic patterns.
from dataclasses import dataclass, field
from typing import Any
from .._types import ChatMessage
from agent_framework._types import ChatMessage
def _new_chat_message_list() -> list[ChatMessage]:
@@ -57,7 +57,7 @@ class OrchestrationState:
Returns:
Dict with encoded conversation and metadata for persistence
"""
from ._conversation_state import encode_chat_messages
from agent_framework._workflows._conversation_state import encode_chat_messages
result: dict[str, Any] = {
"conversation": encode_chat_messages(self.conversation),
@@ -78,7 +78,7 @@ class OrchestrationState:
Returns:
Restored OrchestrationState instance
"""
from ._conversation_state import decode_chat_messages
from agent_framework._workflows._conversation_state import decode_chat_messages
task = None
if "task" in data:
@@ -8,7 +8,7 @@ No inheritance required - just import and call.
import logging
from .._types import ChatMessage
from agent_framework._types import ChatMessage
logger = logging.getLogger(__name__)
@@ -53,11 +53,12 @@ from agent_framework._workflows._executor import (
handler,
)
from agent_framework._workflows._message_utils import normalize_messages_input
from agent_framework._workflows._orchestration_request_info import AgentApprovalExecutor
from agent_framework._workflows._workflow import Workflow
from agent_framework._workflows._workflow_builder import WorkflowBuilder
from agent_framework._workflows._workflow_context import WorkflowContext
from ._orchestration_request_info import AgentApprovalExecutor
logger = logging.getLogger(__name__)
@@ -235,7 +236,7 @@ class SequentialBuilder:
Returns:
Self for fluent chaining
"""
from agent_framework._workflows._orchestration_request_info import resolve_request_info_filter
from ._orchestration_request_info import resolve_request_info_filter
self._request_info_enabled = True
self._request_info_filter = resolve_request_info_filter(list(agents) if agents else None)
@@ -6,12 +6,10 @@ from typing import Any, cast
import pytest
from agent_framework import (
AgentExecutorResponse,
AgentRequestInfoResponse,
AgentResponse,
AgentResponseUpdate,
AgentThread,
BaseAgent,
BaseGroupChatOrchestrator,
ChatAgent,
ChatMessage,
ChatResponse,
@@ -24,6 +22,8 @@ from agent_framework import (
)
from agent_framework._workflows._checkpoint import InMemoryCheckpointStorage
from agent_framework.orchestrations import (
AgentRequestInfoResponse,
BaseGroupChatOrchestrator,
GroupChatBuilder,
GroupChatState,
MagenticContext,
@@ -1143,9 +1143,10 @@ def test_group_chat_with_orchestrator_factory_returning_base_orchestrator():
def orchestrator_factory() -> BaseGroupChatOrchestrator:
nonlocal factory_call_count
factory_call_count += 1
from agent_framework._workflows._base_group_chat_orchestrator import ParticipantRegistry
from agent_framework.orchestrations import GroupChatOrchestrator
from agent_framework_orchestrations._base_group_chat_orchestrator import ParticipantRegistry
# Create a custom orchestrator; when returning BaseGroupChatOrchestrator,
# the builder uses it as-is without modifying its participant registry
return GroupChatOrchestrator(
@@ -15,7 +15,6 @@ from agent_framework import (
ChatMessage,
Content,
Executor,
GroupChatRequestMessage,
RequestInfoEvent,
Workflow,
WorkflowCheckpoint,
@@ -29,6 +28,7 @@ from agent_framework import (
)
from agent_framework._workflows._checkpoint import InMemoryCheckpointStorage
from agent_framework.orchestrations import (
GroupChatRequestMessage,
MagenticBuilder,
MagenticContext,
MagenticManagerBase,
@@ -7,7 +7,6 @@ from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
from agent_framework import (
AgentProtocol,
AgentResponse,
@@ -16,13 +15,14 @@ from agent_framework import (
ChatMessage,
)
from agent_framework._workflows._agent_executor import AgentExecutorRequest, AgentExecutorResponse
from agent_framework._workflows._orchestration_request_info import (
from agent_framework._workflows._workflow_context import WorkflowContext
from agent_framework_orchestrations._orchestration_request_info import (
AgentApprovalExecutor,
AgentRequestInfoExecutor,
AgentRequestInfoResponse,
resolve_request_info_filter,
)
from agent_framework._workflows._workflow_context import WorkflowContext
class TestResolveRequestInfoFilter:
@@ -3,7 +3,7 @@
import asyncio
import json
from dataclasses import dataclass
from typing import Annotated, Never
from typing import Annotated
from agent_framework import (
AgentExecutorResponse,
@@ -16,6 +16,7 @@ from agent_framework import (
tool,
)
from agent_framework.openai import OpenAIChatClient
from typing_extensions import Never
"""
Sample: Agents in a workflow with AI functions requiring approval
@@ -26,15 +26,14 @@ from collections.abc import AsyncIterable
from typing import Any
from agent_framework import (
AgentRequestInfoResponse,
ChatMessage,
ConcurrentBuilder,
RequestInfoEvent,
WorkflowEvent,
WorkflowOutputEvent,
)
from agent_framework._workflows._agent_executor import AgentExecutorResponse
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import AgentRequestInfoResponse, ConcurrentBuilder
from azure.identity import AzureCliCredential
# Store chat client at module level for aggregator access
@@ -28,14 +28,13 @@ from typing import cast
from agent_framework import (
AgentExecutorResponse,
AgentRequestInfoResponse,
ChatMessage,
GroupChatBuilder,
RequestInfoEvent,
WorkflowEvent,
WorkflowOutputEvent,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import AgentRequestInfoResponse, GroupChatBuilder
from azure.identity import AzureCliCredential
@@ -27,14 +27,13 @@ from typing import cast
from agent_framework import (
AgentExecutorResponse,
AgentRequestInfoResponse,
ChatMessage,
RequestInfoEvent,
SequentialBuilder,
WorkflowEvent,
WorkflowOutputEvent,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import AgentRequestInfoResponse, SequentialBuilder
from azure.identity import AzureCliCredential