mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: [BREAKING] Standardize orchestration terminal outputs as AgentResponse (#5301)
* Fix orchestration outputs so as_agent() returns the final answer only. Align other orchestration outputs * Fix orchestration output issues from review comments 1. Sample cleanup: Remove commented-out FoundryChatClient block and update prerequisites to reference OPENAI_CHAT_MODEL_ID instead of FOUNDRY_* vars. 2. Sequential approval output: Change _EndWithConversation.end_with_agent_executor_response from a no-op sink to yield response.agent_response. When the last participant is AgentApprovalExecutor (via with_request_info), _EndWithConversation is the output executor so the yield produces the terminal answer. When the last participant is a regular AgentExecutor, _EndWithConversation is not in output_executors so the yield is silently filtered out. 3. Forward data events through WorkflowExecutor: _process_workflow_result now also forwards 'data' events from sub-workflows so that emit_intermediate_data=True on AgentExecutor works correctly when wrapped in AgentApprovalExecutor. 4. Concurrent docstring: Update _AggregateAgentConversations docstring to say 'deterministic participant order' instead of 'completion order'. 5. Add test_concurrent_intermediate_outputs_emits_data_events verifying that ConcurrentBuilder(intermediate_outputs=True) emits per-participant data events alongside the single aggregated output event. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add tests for sequential workflow with_request_info and intermediate_outputs (#5301) Address PR review comments 2, 3, and 5: - Add test_sequential_request_info_last_participant_emits_output: Verifies that when the last participant is wrapped via with_request_info() (AgentApprovalExecutor), the workflow still emits a terminal output after approval, exercising the _EndWithConversation.end_with_agent_executor_response fallback path. - Add test_sequential_request_info_with_intermediate_outputs_emits_data_events: Verifies that emit_intermediate_data=True works correctly through AgentApprovalExecutor wrapping—WorkflowExecutor._process_result already forwards data events from sub-workflows, so intermediate agent responses surface as data events in the parent workflow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix pyright type errors from AgentResponse output refactor (#5301) Update cast() calls in _group_chat.py and _magentic.py to use WorkflowContext[Never, AgentResponse] instead of the old WorkflowContext[Never, list[Message]], matching the updated method signatures in _base_group_chat_orchestrator.py. Fix _sequential.py _EndWithConversation.end_with_agent_executor_response to declare WorkflowContext[Any, AgentResponse] so yield_output accepts AgentResponse[None]. Fix _workflow_executor.py data event forwarding to handle nullable executor_id. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix pyright reportUnknownVariableType in _agent.py (#5301) Extract event.data into a typed local variable before the isinstance check to avoid pyright narrowing it to AgentResponse[Unknown]. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix pyright reportMissingImports for orjson in file history samples (#5301) Add pyright: ignore[reportMissingImports] to orjson imports that are already guarded by try/except ImportError, matching the existing pattern used elsewhere in the samples. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review feedback for #5301: review comment fixes * Address review feedback for #5301: review comment fixes * Revert sequential_workflow_as_agent sample to FoundryChatClient Reverts the mistaken switch from FoundryChatClient to OpenAIChatClient in the sequential workflow as agent sample. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address ultrareview feedback: emit_data_events rename + WorkflowAgent reasoning conversion Layered on top of the prior review-feedback work in this branch. Renames: - AgentExecutor.emit_intermediate_data -> emit_data_events (mechanical rename; orchestration semantics live at the orchestration layer, not the general-purpose executor). Forwarded through MagenticAgentExecutor, AgentApprovalExecutor, and all orchestration call sites. - HandoffAgentExecutor._check_terminate_and_yield -> _should_terminate (pure predicate; no longer yields anything). HandoffBuilder docstring rewritten to describe the new per-agent AgentResponse output contract. WorkflowAgent reasoning-content conversion: - Add _rewrite_text_to_reasoning(contents) and _msg_as_reasoning(msg) helpers; the as_agent() path now reframes text content from data events as text_reasoning Content blocks before merging into the AgentResponse. - Consumers iterate msg.contents and branch on content.type — same path they already use for Claude thinking and OpenAI reasoning. No new field on Message/AgentResponse/WorkflowEvent. - Streaming branch constructs fresh AgentResponseUpdate instances instead of mutating shared payloads (regression test added). - Helper _msg_maybe_reasoning consolidates the conditional rewrite at three call sites in the non-streaming conversion. Tests: - TestWorkflowAgentReasoningHelpers + TestWorkflowAgentDataEventReasoningConversion add 9 new tests covering helpers, non-streaming, streaming, mixed content, already-reasoning passthrough, and mutation-safety regression. - Updated test_sequential_as_agent_with_intermediate_outputs_includes_chain to assert text_reasoning content for intermediate agents. * Fix pyright: widen event.data to Any to avoid partial-unknown narrowing The streaming conversion path narrowed event.data via isinstance against generic AgentResponse, producing AgentResponse[Unknown] and tripping reportUnknownVariableType/reportUnknownMemberType. Binding data: Any before the check keeps runtime behavior identical while restoring a fully known type for downstream access. * Clean up design * Scope to agent output semantics only * yield AgentResponseUpdate streaming, AgentResponse non-streaming * Fix mypy/pyright: widen cast types at GroupChat callsites Eight callsites in _group_chat.py still cast to WorkflowContext[Never, AgentResponse] but the base orchestrator methods now accept the wider WorkflowContext[Never, AgentResponse | AgentResponseUpdate] (mode-aware yields). W_OutT is invariant, so the narrower cast is not assignable. Magentic was widened in the same commit; this catches the GroupChat callsites that were missed. * Python: skip flaky Foundry / Foundry Hosting integration tests (#5553) These two integration tests have been failing in the merge queue across multiple unrelated PRs (5301, 5531). Both are marked `@pytest.mark.flaky` with 3 retries, but all attempts fail back-to-back. Skipping both with a reason pointing to #5553 so they can be fixed properly without continuing to block unrelated merges. - packages/foundry_hosting/tests/test_responses_int.py::TestOptions::test_temperature_and_max_tokens - packages/foundry/tests/foundry/test_foundry_embedding_client.py::TestFoundryEmbeddingIntegration::test_text_embedding_live Also includes a one-line uv.lock specifier-ordering normalization auto-applied by the poe-check pre-commit hook. --------- Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
40e90c96c3
commit
866a325b48
@@ -26,8 +26,8 @@ Note on internal adapters:
|
||||
You can safely ignore them when focusing on agent progress.
|
||||
|
||||
Prerequisites:
|
||||
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
|
||||
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
|
||||
- FOUNDRY_PROJECT_ENDPOINT must be set to the Azure Foundry project endpoint.
|
||||
- FOUNDRY_MODEL must be set to the model name for the Foundry chat client.
|
||||
"""
|
||||
|
||||
|
||||
@@ -68,28 +68,18 @@ async def main() -> None:
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
===== Final Conversation =====
|
||||
===== Conversation =====
|
||||
------------------------------------------------------------
|
||||
01 [user]
|
||||
Write a tagline for a budget-friendly eBike.
|
||||
------------------------------------------------------------
|
||||
02 [writer]
|
||||
Ride farther, spend less—your affordable eBike adventure starts here.
|
||||
------------------------------------------------------------
|
||||
03 [reviewer]
|
||||
This tagline clearly communicates affordability and the benefit of extended travel, making it
|
||||
appealing to budget-conscious consumers. It has a friendly and motivating tone, though it could
|
||||
be slightly shorter for more punch. Overall, a strong and effective suggestion!
|
||||
|
||||
===== as_agent() Conversation =====
|
||||
------------------------------------------------------------
|
||||
01 [writer]
|
||||
Go electric, save big—your affordable ride awaits!
|
||||
------------------------------------------------------------
|
||||
02 [reviewer]
|
||||
01 [reviewer]
|
||||
Catchy and straightforward! The tagline clearly emphasizes both the electric aspect and the affordability of the
|
||||
eBike. It's inviting and actionable. For even more impact, consider making it slightly shorter:
|
||||
"Go electric, save big." Overall, this is an effective and appealing suggestion for a budget-friendly eBike.
|
||||
|
||||
Note:
|
||||
`workflow.as_agent()` returns ONLY the final agent's response (the "answer") — the prior agents' work
|
||||
is not included in the response. To observe intermediate agents while running as an agent, build with
|
||||
`SequentialBuilder(participants=[...], intermediate_outputs=True)`; the intermediate replies are then
|
||||
surfaced as `data` events and merged into the AgentResponse.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import (
|
||||
Agent,
|
||||
AgentExecutorResponse,
|
||||
AgentResponse,
|
||||
Executor,
|
||||
Message,
|
||||
WorkflowContext,
|
||||
@@ -16,6 +16,7 @@ from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.orchestrations import SequentialBuilder
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from typing_extensions import Never
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
@@ -25,13 +26,14 @@ Sample: Sequential workflow mixing agents and a custom summarizer executor
|
||||
|
||||
This demonstrates how SequentialBuilder chains participants with a shared
|
||||
conversation context (list[Message]). An agent produces content; a custom
|
||||
executor appends a compact summary to the conversation. The workflow completes
|
||||
after all participants have executed in sequence, and the final output contains
|
||||
the complete conversation.
|
||||
executor synthesizes a compact summary and yields it as the workflow's terminal
|
||||
output.
|
||||
|
||||
Custom executor contract:
|
||||
- Provide at least one @handler accepting AgentExecutorResponse and a WorkflowContext[list[Message]]
|
||||
- Emit the updated conversation via ctx.send_message([...])
|
||||
- Intermediate custom executors: handle the message type from the prior participant
|
||||
and forward `list[Message]` via `ctx.send_message(...)` for the next participant.
|
||||
- Terminator custom executors: handle the message type from the prior participant and
|
||||
yield the workflow's final answer as an `AgentResponse` via `ctx.yield_output(...)`.
|
||||
|
||||
Prerequisites:
|
||||
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
|
||||
@@ -41,27 +43,29 @@ Prerequisites:
|
||||
|
||||
|
||||
class Summarizer(Executor):
|
||||
"""Simple summarizer: consumes full conversation and appends an assistant summary."""
|
||||
"""Terminator custom executor: synthesizes a one-line summary as the workflow's final answer."""
|
||||
|
||||
@handler
|
||||
async def summarize(self, agent_response: AgentExecutorResponse, ctx: WorkflowContext[list[Message]]) -> None:
|
||||
"""Append a summary message to a copy of the full conversation.
|
||||
async def summarize(
|
||||
self,
|
||||
agent_response: AgentExecutorResponse,
|
||||
ctx: WorkflowContext[Never, AgentResponse],
|
||||
) -> None:
|
||||
"""Yield a terminal AgentResponse containing the summary.
|
||||
|
||||
Note: A custom executor must be able to handle the message type from the prior participant, and produce
|
||||
the message type expected by the next participant. In this case, the prior participant is an agent thus
|
||||
the input is AgentExecutorResponse (an agent will be wrapped in an AgentExecutor, which produces
|
||||
`AgentExecutorResponse`). If the next participant is also an agent or this is the final participant,
|
||||
the output must be `list[Message]`.
|
||||
The prior participant is an agent, which is wrapped in an `AgentExecutor` that
|
||||
produces `AgentExecutorResponse`. As the last participant in the sequential workflow,
|
||||
this executor calls `ctx.yield_output(AgentResponse(...))` so its output becomes the
|
||||
workflow's terminal output (rather than being forwarded to a downstream participant).
|
||||
"""
|
||||
if not agent_response.full_conversation:
|
||||
await ctx.send_message([Message("assistant", ["No conversation to summarize."])])
|
||||
await ctx.yield_output(AgentResponse(messages=[Message("assistant", ["No conversation to summarize."])]))
|
||||
return
|
||||
|
||||
users = sum(1 for m in agent_response.full_conversation if m.role == "user")
|
||||
assistants = sum(1 for m in agent_response.full_conversation if m.role == "assistant")
|
||||
summary = Message("assistant", [f"Summary -> users:{users} assistants:{assistants}"])
|
||||
final_conversation = list(agent_response.full_conversation) + [summary]
|
||||
await ctx.send_message(final_conversation)
|
||||
await ctx.yield_output(AgentResponse(messages=[summary]))
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
@@ -81,33 +85,20 @@ async def main() -> None:
|
||||
summarizer = Summarizer(id="summarizer")
|
||||
workflow = SequentialBuilder(participants=[content, summarizer]).build()
|
||||
|
||||
# 3) Run workflow and extract final conversation
|
||||
# 3) Run workflow and extract the final summary
|
||||
events = await workflow.run("Explain the benefits of budget eBikes for commuters.")
|
||||
outputs = events.get_outputs()
|
||||
|
||||
if outputs:
|
||||
print("===== Final Conversation =====")
|
||||
messages: list[Message] | Any = outputs[0]
|
||||
for i, msg in enumerate(messages, start=1):
|
||||
name = msg.author_name or ("assistant" if msg.role == "assistant" else "user")
|
||||
print(f"{'-' * 60}\n{i:02d} [{name}]\n{msg.text}")
|
||||
print("===== Final Summary =====")
|
||||
final: AgentResponse = outputs[0]
|
||||
for msg in final.messages:
|
||||
print(msg.text)
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
------------------------------------------------------------
|
||||
01 [user]
|
||||
Explain the benefits of budget eBikes for commuters.
|
||||
------------------------------------------------------------
|
||||
02 [content]
|
||||
Budget eBikes offer commuters an affordable, eco-friendly alternative to cars and public transport.
|
||||
Their electric assistance reduces physical strain and allows riders to cover longer distances quickly,
|
||||
minimizing travel time and fatigue. Budget models are low-cost to maintain and operate, making them accessible
|
||||
for a wider range of people. Additionally, eBikes help reduce traffic congestion and carbon emissions,
|
||||
supporting greener urban environments. Overall, budget eBikes provide cost-effective, efficient, and
|
||||
sustainable transportation for daily commuting needs.
|
||||
------------------------------------------------------------
|
||||
03 [assistant]
|
||||
===== Final Summary =====
|
||||
Summary -> users:1 assistants:1
|
||||
"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user