Files
agent-framework/python/samples/03-workflows/orchestrations/handoff_autonomous.py
T
Eduard van Valkenburg a2856d3b92 Python: restructure: Python samples into progressive 01-05 layout (#3862)
* restructure: Python samples into progressive 01-05 layout

- 01-get-started/: 6 numbered steps (hello agent → hosting)
- 02-agents/: all agent concept samples (tools, middleware, providers, etc.)
- 03-workflows/: ALL existing workflow samples preserved as-is
- 04-hosting/: azure-functions, durabletask, a2a
- 05-end-to-end/: demos, evaluation, hosted agents
- Old files moved to _to_delete/ for review
- Added AGENTS.md with structure documentation
- autogen-migration/ and semantic-kernel-migration/ preserved at root

* fix: switch to AzureOpenAI Foundry, fix CI failures

- Switch all 01-get-started samples to AzureOpenAIResponsesClient with
  Azure AI Foundry project endpoint (AZURE_AI_PROJECT_ENDPOINT +
  AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME + AzureCliCredential)
- Add _to_delete/ and 05-end-to-end/ to pyrightconfig.samples.json excludes
- Fix test paths in packages/ that referenced old getting_started/ dirs:
  durabletask conftest + streaming test, azurefunctions conftest,
  devui conftest + capture_messages + openai_sdk_integration
- Fix workflow_as_agent_human_in_the_loop.py import (sibling import)
- Update hosting READMEs and tool comment paths
- Replace root README.md with new structure overview
- Update AGENTS.md to document Azure OpenAI Foundry as default provider

* cleanup: remove _to_delete folder, copy resource files to active dirs

All files in _to_delete/ were either:
- Exact duplicates of files in the new structure (240 files)
- Same file with only comment path updates (100 files)
- One import-fix diff (workflow_as_agent_human_in_the_loop.py)
- One superseded minimal_sample.py

Resource files (sample.pdf, countries.json, employees.pdf, weather.json)
copied to 02-agents/sample_assets/ and 02-agents/resources/ since active
samples reference them.

* fix: address PR review comments, centralize resources, remove root duplicates

- Fix type annotation in 04_memory.py (string union -> proper types)
- Fix old sample paths in observability files
- Fix grammar/spelling in observability samples
- Move sample_assets/ and resources/ to shared/ folder
- Remove 8 duplicate observability files from 02-agents root
- Update resource path references in multimodal_input and provider samples

* fix: update broken links from old getting_started paths to new structure

- Update relative paths in READMEs: getting_started/ → 01-get-started/,
  02-agents/, 03-workflows/, 04-hosting/, 05-end-to-end/
- Fix absolute GitHub URLs in package READMEs
- Fix broken link in ollama package README

* fix: convert absolute GitHub URLs to relative paths for link checker

Absolute URLs to python/samples/ on main branch 404 until PR merges.
Converted to relative paths that linkspector can verify locally.

* fix: update link for handoff sample moved to orchestrations/

* fix: update chatkit-integration README path from demos/ to 05-end-to-end/

* fix: update broken links in orchestrations README to match flat directory structure
2026-02-12 17:36:36 +00:00

152 lines
6.2 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
import asyncio
import logging
from typing import cast
from agent_framework import (
Agent,
AgentResponseUpdate,
Message,
resolve_agent_id,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import HandoffBuilder
from azure.identity import AzureCliCredential
logging.basicConfig(level=logging.ERROR)
"""Sample: Autonomous handoff workflow with agent iteration.
This sample demonstrates `.with_autonomous_mode()`, where agents continue
iterating on their task until they explicitly invoke a handoff tool. This allows
specialists to perform long-running autonomous work (research, coding, analysis)
without prematurely returning control to the coordinator or user.
Routing Pattern:
User -> Coordinator -> Specialist (iterates N times) -> Handoff -> Final Output
Prerequisites:
- `az login` (Azure CLI authentication)
- Environment variables for AzureOpenAIChatClient (AZURE_OPENAI_ENDPOINT, etc.)
Key Concepts:
- Autonomous interaction mode: agents iterate until they handoff
- Turn limits: use `.with_autonomous_mode(turn_limits={agent_name: N})` to cap iterations per agent
"""
def create_agents(
client: AzureOpenAIChatClient,
) -> tuple[Agent, Agent, Agent]:
"""Create coordinator and specialists for autonomous iteration."""
coordinator = client.as_agent(
instructions=(
"You are a coordinator. You break down a user query into a research task and a summary task. "
"Assign the two tasks to the appropriate specialists, one after the other."
),
name="coordinator",
)
research_agent = client.as_agent(
instructions=(
"You are a research specialist that explores topics thoroughly using web search. "
"When given a research task, break it down into multiple aspects and explore each one. "
"Continue your research across multiple responses - don't try to finish everything in one "
"response. After each response, think about what else needs to be explored. When you have "
"covered the topic comprehensively (at least 3-4 different aspects), return control to the "
"coordinator. Keep each individual response focused on one aspect."
),
name="research_agent",
)
summary_agent = client.as_agent(
instructions=(
"You summarize research findings. Provide a concise, well-organized summary. When done, return "
"control to the coordinator."
),
name="summary_agent",
)
return coordinator, research_agent, summary_agent
async def main() -> None:
"""Run an autonomous handoff workflow with specialist iteration enabled."""
client = AzureOpenAIChatClient(credential=AzureCliCredential())
coordinator, research_agent, summary_agent = create_agents(client)
# Build the workflow with autonomous mode
# In autonomous mode, agents continue iterating until they invoke a handoff tool
# termination_condition: Terminate after coordinator provides 5 assistant responses
workflow = (
HandoffBuilder(
name="autonomous_iteration_handoff",
participants=[coordinator, research_agent, summary_agent],
termination_condition=lambda conv: (
sum(1 for msg in conv if msg.author_name == "coordinator" and msg.role == "assistant") >= 5
),
)
.with_start_agent(coordinator)
.add_handoff(coordinator, [research_agent, summary_agent])
.add_handoff(research_agent, [coordinator]) # Research can hand back to coordinator
.add_handoff(summary_agent, [coordinator])
.with_autonomous_mode(
# You can set turn limits per agent to allow some agents to go longer.
# If a limit is not set, the agent will get an default limit: 50.
# Internally, handoff prefers agent names as the agent identifiers if set.
# Otherwise, it falls back to agent IDs.
turn_limits={
resolve_agent_id(coordinator): 5,
resolve_agent_id(research_agent): 10,
resolve_agent_id(summary_agent): 5,
}
)
.build()
)
request = "Perform a comprehensive research on Microsoft Agent Framework."
print("Request:", request)
last_response_id: str | None = None
async for event in workflow.run(request, stream=True):
if event.type == "handoff_sent":
print(f"\nHandoff Event: from {event.data.source} to {event.data.target}\n")
elif event.type == "output":
data = event.data
if isinstance(data, AgentResponseUpdate):
if not data.text:
# Skip updates that don't have text content
# These can be tool calls or other non-text events
continue
rid = data.response_id
if rid != last_response_id:
if last_response_id is not None:
print("\n")
print(f"{data.author_name}:", end=" ", flush=True)
last_response_id = rid
print(data.text, end="", flush=True)
elif event.type == "output":
# The output of the handoff workflow is a collection of chat messages from all participants
outputs = cast(list[Message], event.data)
print("\n" + "=" * 80)
print("\nFinal Conversation Transcript:\n")
for message in outputs:
print(f"{message.author_name or message.role}: {message.text}\n")
"""
Expected behavior:
- Coordinator routes to research_agent.
- Research agent iterates multiple times, exploring different aspects of Microsoft Agent Framework.
- Each iteration adds to the conversation without returning to coordinator.
- After thorough research, research_agent calls handoff to coordinator.
- Coordinator routes to summary_agent for final summary.
In autonomous mode, agents continue working until they invoke a handoff tool,
allowing the research_agent to perform 3-4+ responses before handing off.
"""
if __name__ == "__main__":
asyncio.run(main())