mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
b05fc9e849
* Initial plan * Add load_dotenv() to 303 Python samples for environment variable loading Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Update SAMPLE_GUIDELINES.md to document load_dotenv() requirement Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Update samples README.md to document .env file usage Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Run ruff format on all changed sample files Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Clarify load_dotenv() usage in README - local dev vs production Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Remove deprecated getting_started folder as requested Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Document env_file_path parameter for per-client configuration Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Merge main branch to resolve conflicts Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Fix run_evaluation.py file that was empty in merge commit Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Remove dotnet changes from merge - out of scope for this PR Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Remove package and test changes from merge - only sample changes needed Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Remove test_func_utils.py - only sample changes needed Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Revert sample files not in original changeset - keep only load_dotenv additions Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Move load_dotenv() outside snippet tag in 06_host_your_agent.py Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Fix comment placement - move load_dotenv before code comments Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Fix load_dotenv() placement across all samples - after docstring, before code comments Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Merge latest main branch with load_dotenv changes Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Remove non-sample changes from merge - keep only load_dotenv additions Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Revert non-load_dotenv sample changes from merge Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Fix run_evaluation.py - use main's improved version (file already had load_dotenv) Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Manual update * Manual update 2 * Fix Role usage and load_dotenv placement per PR review feedback Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com> * Fix Role usage - use string literals not enum attributes Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com> * Fix SAMPLE_GUIDELINES.md example - load_dotenv before docstring per guidance Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com> * Move load_dotenv() before docstrings in all samples per SAMPLE_GUIDELINES ordering Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com> * Address PR review: rename files, fix placement, add session usage, remove note Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com> * Update Redis README to reference renamed file redis_history_provider.py Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> Co-authored-by: Tao Chen <taochen@microsoft.com> Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com> Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
156 lines
6.3 KiB
Python
156 lines
6.3 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
|
|
from dotenv import load_dotenv
|
|
|
|
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
|
|
"""
|
|
|
|
# Load environment variables from .env file
|
|
load_dotenv()
|
|
|
|
|
|
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())
|