Python: Add support for the MagenticWorkflowBuilder (#496)

* magentic happy path - wip

* Support workflow high-level magentic builder API. Add tests and samples.

* Add sample docstring

* Addressing PR feedback round 1

* Fix mypy errors

* Callback improvements

* Rename to MagenticBuilder

* Improvements

* Emit function calling deltas

* PR feedback 2

* Clean up sample
This commit is contained in:
Evan Mattson
2025-08-28 14:39:48 +09:00
committed by GitHub
Unverified
parent 738866f4fe
commit 529341f58b
8 changed files with 2716 additions and 5 deletions
@@ -0,0 +1,148 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import logging
from agent_framework import ChatClientAgent, HostedCodeInterpreterTool
from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient
from agent_framework_workflow import (
MagenticAgentDeltaEvent,
MagenticAgentMessageEvent,
MagenticBuilder,
MagenticCallbackEvent,
MagenticCallbackMode,
MagenticFinalResultEvent,
MagenticOrchestratorMessageEvent,
WorkflowCompletedEvent,
)
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
"""
Magentic Workflow (multi-agent) sample.
This sample shows how to orchestrate multiple agents using the
MagenticBuilder:
- ResearcherAgent (ChatClientAgent backed by an OpenAI chat client) for
finding information.
- CoderAgent (ChatClientAgent backed by OpenAI Assistants with the hosted
code interpreter tool) for analysis and computation.
The workflow is configured with:
- A Standard Magentic manager (uses a chat client for planning and progress).
- Callbacks for final results, per-message agent responses, and streaming
token updates.
When run, the script builds the workflow, submits a task about estimating the
energy efficiency and CO2 emissions of several ML models, streams intermediate
events to the console, and prints the final aggregated answer at completion.
"""
async def main() -> None:
researcher_agent = ChatClientAgent(
name="ResearcherAgent",
description="Specialist in research and information gathering",
instructions=(
"You are a Researcher. You find information without additional computation or quantitative analysis."
),
# This agent requires the gpt-4o-search-preview model to perform web searches.
# Feel free to explore with other agents that support web search, for example,
# the `OpenAIResponseAgent` or `AzureAIAgent` with bing grounding.
chat_client=OpenAIChatClient(ai_model_id="gpt-4o-search-preview"),
)
coder_agent = ChatClientAgent(
name="CoderAgent",
description="A helpful assistant that writes and executes code to process and analyze data.",
instructions="You solve questions using code. Please provide detailed analysis and computation process.",
chat_client=OpenAIResponsesClient(),
tools=HostedCodeInterpreterTool(),
)
# Unified callback
async def on_event(event: MagenticCallbackEvent) -> None:
"""
The `on_event` callback processes events emitted by the workflow.
Events include: orchestrator messages, agent delta updates, agent messages, and final result events.
"""
nonlocal last_stream_agent_id, stream_line_open
if isinstance(event, MagenticOrchestratorMessageEvent):
print(f"\n[ORCH:{event.kind}]\n\n{getattr(event.message, 'text', '')}\n{'-' * 26}")
elif isinstance(event, MagenticAgentDeltaEvent):
if last_stream_agent_id != event.agent_id or not stream_line_open:
if stream_line_open:
print()
print(f"\n[STREAM:{event.agent_id}]: ", end="", flush=True)
last_stream_agent_id = event.agent_id
stream_line_open = True
print(event.text, end="", flush=True)
elif isinstance(event, MagenticAgentMessageEvent):
if stream_line_open:
print(" (final)")
stream_line_open = False
print()
msg = event.message
if msg is not None:
response_text = (msg.text or "").replace("\n", " ")
print(f"\n[AGENT:{event.agent_id}] {msg.role.value}\n\n{response_text}\n{'-' * 26}")
elif isinstance(event, MagenticFinalResultEvent):
print("\n" + "=" * 50)
print("FINAL RESULT:")
print("=" * 50)
if event.message is not None:
print(event.message.text)
print("=" * 50)
print("\nBuilding Magentic Workflow...")
# State used by on_agent_stream callback
last_stream_agent_id: str | None = None
stream_line_open: bool = False
workflow = (
MagenticBuilder()
.participants(researcher=researcher_agent, coder=coder_agent)
.on_event(on_event, mode=MagenticCallbackMode.STREAMING)
.with_standard_manager(
chat_client=OpenAIChatClient(),
max_round_count=10,
max_stall_count=3,
max_reset_count=2,
)
.build()
)
task = (
"I am preparing a report on the energy efficiency of different machine learning model architectures. "
"Compare the estimated training and inference energy consumption of ResNet-50, BERT-base, and GPT-2 "
"on standard datasets (e.g., ImageNet for ResNet, GLUE for BERT, WebText for GPT-2). "
"Then, estimate the CO2 emissions associated with each, assuming training on an Azure Standard_NC6s_v3 "
"VM for 24 hours. Provide tables for clarity, and recommend the most energy-efficient model "
"per task type (image classification, text classification, and text generation)."
)
print(f"\nTask: {task}")
print("\nStarting workflow execution...")
try:
completion_event = None
async for event in workflow.run_streaming(task):
print(f"Event: {event}")
if isinstance(event, WorkflowCompletedEvent):
completion_event = event
if completion_event is not None:
data = getattr(completion_event, "data", None)
preview = getattr(data, "text", None) or (str(data) if data is not None else "")
print(f"Workflow completed with result:\n\n{preview}")
except Exception as e:
print(f"Workflow execution failed: {e}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,189 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import logging
from typing import cast
from agent_framework import ChatClientAgent, HostedCodeInterpreterTool
from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient
from agent_framework_workflow import (
MagenticAgentDeltaEvent,
MagenticAgentMessageEvent,
MagenticBuilder,
MagenticCallbackEvent,
MagenticCallbackMode,
MagenticFinalResultEvent,
MagenticOrchestratorMessageEvent,
MagenticPlanReviewDecision,
MagenticPlanReviewReply,
MagenticPlanReviewRequest,
RequestInfoEvent,
WorkflowCompletedEvent,
)
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
"""
Magentic workflow with human-in-the-loop plan review and update.
This sample builds a Magentic workflow with two cooperating agents and enables
plan review so a human can approve or revise the plan before execution:
- researcher: ChatClientAgent backed by OpenAIChatClient (web/search-capable model)
- coder: ChatClientAgent backed by OpenAIAssistantsClient with the Hosted Code Interpreter tool
Key behaviors demonstrated:
- with_plan_review(): requests a PlanReviewRequest before coordination begins
- Event loop that waits for RequestInfoEvent[PlanReviewRequest], prints the plan, then
replies with PlanReviewReply (here we auto-approve, but you can edit/collect input)
- Callbacks: on_agent_stream (incremental chunks), on_agent_response (final messages),
on_result (final answer), and on_exception
Prereqs: configure your OpenAI credentials in the environment so the Chat/Assistants
clients can run. You can swap clients/models as needed.
"""
async def main() -> None:
researcher_agent = ChatClientAgent(
name="ResearcherAgent",
description="Specialist in research and information gathering",
instructions=(
"You are a Researcher. You find information without additional computation or quantitative analysis."
),
# This agent requires the gpt-4o-search-preview model to perform web searches.
# Feel free to explore with other agents that support web search, for example,
# the `OpenAIResponseAgent` or `AzureAIAgent` with bing grounding.
chat_client=OpenAIChatClient(ai_model_id="gpt-4o-search-preview"),
)
coder_agent = ChatClientAgent(
name="CoderAgent",
description="A helpful assistant that writes and executes code to process and analyze data.",
instructions="You solve questions using code. Please provide detailed analysis and computation process.",
chat_client=OpenAIResponsesClient(),
tools=HostedCodeInterpreterTool(),
)
# Callbacks
def on_exception(exception: Exception) -> None:
print(f"Exception occurred: {exception}")
logger.exception("Workflow exception", exc_info=exception)
# Unified callback
async def on_event(event: MagenticCallbackEvent) -> None:
nonlocal last_stream_agent_id, stream_line_open
if isinstance(event, MagenticOrchestratorMessageEvent):
print(f"\n[ORCH:{event.kind}]\n\n{getattr(event.message, 'text', '')}\n{'-' * 26}")
elif isinstance(event, MagenticAgentDeltaEvent):
if last_stream_agent_id != event.agent_id or not stream_line_open:
if stream_line_open:
print()
print(f"\n[STREAM:{event.agent_id}]: ", end="", flush=True)
last_stream_agent_id = event.agent_id
stream_line_open = True
print(event.text, end="", flush=True)
elif isinstance(event, MagenticAgentMessageEvent):
if stream_line_open:
print(" (final)")
stream_line_open = False
print()
msg = event.message
if msg is not None:
response_text = (msg.text or "").replace("\n", " ")
print(f"\n[AGENT:{event.agent_id}] {msg.role.value}\n\n{response_text}\n{'-' * 26}")
elif isinstance(event, MagenticFinalResultEvent):
print("\n" + "=" * 50)
print("FINAL RESULT:")
print("=" * 50)
if event.message is not None:
print(event.message.text)
print("=" * 50)
print("\nBuilding Magentic Workflow...")
last_stream_agent_id: str | None = None
stream_line_open: bool = False
workflow = (
MagenticBuilder()
.participants(researcher=researcher_agent, coder=coder_agent)
.on_exception(on_exception)
.on_event(on_event, mode=MagenticCallbackMode.STREAMING)
.with_standard_manager(
chat_client=OpenAIChatClient(),
max_round_count=10,
max_stall_count=3,
max_reset_count=2,
)
.with_plan_review()
.build()
)
task = (
"I am preparing a report on the energy efficiency of different machine learning model architectures. "
"Compare the estimated training and inference energy consumption of ResNet-50, BERT-base, and GPT-2 "
"on standard datasets (e.g., ImageNet for ResNet, GLUE for BERT, WebText for GPT-2). "
"Then, estimate the CO2 emissions associated with each, assuming training on an Azure Standard_NC6s_v3 "
"VM for 24 hours. Provide tables for clarity, and recommend the most energy-efficient model "
"per task type (image classification, text classification, and text generation)."
)
print(f"\nTask: {task}")
print("\nStarting workflow execution...")
try:
completion_event: WorkflowCompletedEvent | None = None
pending_request: RequestInfoEvent | None = None
while True:
# Phase 1: run until either completion or a HIL request
if pending_request is None:
async for event in workflow.run_streaming(task):
print(f"Event: {event}")
if isinstance(event, WorkflowCompletedEvent):
completion_event = event
if isinstance(event, RequestInfoEvent) and event.request_type is MagenticPlanReviewRequest:
pending_request = event
review_req = cast(MagenticPlanReviewRequest, event.data)
if review_req.plan_text:
print(f"\n=== PLAN REVIEW REQUEST ===\n{review_req.plan_text}\n")
# Break if completed
if completion_event is not None:
data = getattr(completion_event, "data", None)
preview = getattr(data, "text", None) or (str(data) if data is not None else "")
print(f"Workflow completed with result:\n\n{preview}")
# Phase 2: respond to the pending plan review (HIL) request
if pending_request is not None:
# For demo purposes we approve as-is. Replace this with UI input
# to collect a human decision/comments/edited plan.
reply = MagenticPlanReviewReply(decision=MagenticPlanReviewDecision.APPROVE)
async for event in workflow.send_responses_streaming({pending_request.request_id: reply}):
print(f"Event: {event}")
if isinstance(event, WorkflowCompletedEvent):
completion_event = event
if isinstance(event, RequestInfoEvent) and event.request_type is MagenticPlanReviewRequest:
# Another review cycle requested; keep pending
pending_request = event
review_req = cast(MagenticPlanReviewRequest, event.data)
if review_req.plan_text:
print(f"\n=== PLAN REVIEW REQUEST ===\n{review_req.plan_text}\n")
else:
# Clear pending if no immediate new request
pending_request = None
except Exception as e:
print(f"Workflow execution failed: {e}")
on_exception(e)
if __name__ == "__main__":
asyncio.run(main())