Files
Eduard van Valkenburg 5e056b672e Python: [BREAKING] Python: Provider-leading client design & OpenAI package extraction (#4818)
* Python: Provider-leading client design & OpenAI package extraction

Major refactoring of the Python Agent Framework client architecture:

- Extract OpenAI clients into new `agent-framework-openai` package
- Core package no longer depends on openai, azure-identity, azure-ai-projects
- Rename clients for discoverability: OpenAIResponsesClient → OpenAIChatClient,
  OpenAIChatClient → OpenAIChatCompletionClient
- Unify `model_id`/`deployment_name`/`model_deployment_name` → `model` param
- New FoundryChatClient for Azure AI Foundry Responses API
- New FoundryAgent/FoundryAgentClient for connecting to pre-configured Foundry agents
- Remove OpenAIBase/OpenAIConfigMixin from non-deprecated client MRO
- Deprecate AzureOpenAI* clients, AzureAIClient, OpenAIAssistantsClient
- Reorganize samples: azure_openai+azure_ai+azure_ai_agent → azure/
- ADR-0020: Provider-Leading Client Design

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: missing Agent imports in samples, .model_id → .model in foundry_local sample

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: CI failures — mypy errors, coverage targets, sample imports

- azure-ai mypy: add type ignores for TypedDict total=, model arg, forward ref
- Coverage: replace core.azure/openai targets with openai package target
- project_provider: add type annotation for opts dict

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: populate openai .pyi stub, fix broken README links, coverage targets

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fixes

* updated observabilitty

* reset azure init.pyi

* fix errors

* updated adr number

* fix foundry local

* fixed not renamed docstrings and comments, and added deprecated markers to old classes

* fix tests and pyprojects

* fix test vars

* updated function tests

* update durable

* updated test setup for functions

* Fix Foundry auth in workflow samples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Stabilize Python integration workflows

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update hosting samples for Foundry

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Trigger full CI rerun

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Trigger CI rerun again

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* trigger rerun

* trigger rerun

* fix for litellm

* undo durabletask changes

* Move Foundry APIs into foundry namespace

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix Foundry pyproject formatting

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Split provider samples by Foundry surface

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Restore hosting sample requirements

Also fix the Foundry Local sample link after the provider sample move.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* updated tests

* udpated foundry integration tests

* removed dist from azurefunctions tests

* Use separate Foundry clients for concurrent agents

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix client setup in azfunc and durable

* disabled two tests

* updated setup for some function and durable tests

* improved azure openai setup with new clients

* ignore deprecated

* fixes

* skip 11

* remove openai assistants int tests

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-25 09:56:29 +00:00

246 lines
9.2 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Any
from agent_framework import Agent, AgentResponseUpdate, WorkflowEvent
from dotenv import load_dotenv
"""AutoGen Swarm pattern vs Agent Framework HandoffBuilder.
Demonstrates agent handoff coordination where agents can transfer control
to other specialized agents based on the task requirements.
"""
# Load environment variables from .env file
load_dotenv()
async def run_autogen() -> None:
"""AutoGen's Swarm pattern with human-in-the-loop handoffs."""
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.conditions import HandoffTermination, TextMentionTermination
from autogen_agentchat.messages import HandoffMessage
from autogen_agentchat.teams import Swarm
from autogen_agentchat.ui import Console
from autogen_ext.models.openai import OpenAIChatCompletionClient
client = OpenAIChatCompletionClient(model="gpt-4.1-mini")
# Create triage agent that routes to specialists
triage_agent = AssistantAgent(
name="triage",
model_client=client,
system_message=(
"You are a triage agent. Analyze the user's request and hand off to the appropriate specialist.\n"
"If you need information from the user, first send your message, then handoff to user.\n"
"Use TERMINATE when the issue is fully resolved."
),
handoffs=["billing_agent", "technical_support", "user"],
model_client_stream=True,
)
# Create billing specialist
billing_agent = AssistantAgent(
name="billing_agent",
model_client=client,
system_message=(
"You are a billing specialist. Help with payment and billing questions.\n"
"If you need information from the user, first send your message, then handoff to user.\n"
"When the issue is resolved, handoff to triage to finalize."
),
handoffs=["triage", "user"],
model_client_stream=True,
)
# Create technical support specialist
tech_support = AssistantAgent(
name="technical_support",
model_client=client,
system_message=(
"You are technical support. Help with technical issues.\n"
"If you need information from the user, first send your message, then handoff to user.\n"
"When the issue is resolved, handoff to triage to finalize."
),
handoffs=["triage", "user"],
model_client_stream=True,
)
# Create swarm team with human-in-the-loop termination
termination = HandoffTermination(target="user") | TextMentionTermination("TERMINATE")
team = Swarm(
participants=[triage_agent, billing_agent, tech_support],
termination_condition=termination,
)
# Scripted user responses for demonstration
scripted_responses = [
"I was charged twice for my subscription",
"Yes, the charge of $49.99 appears twice on my credit card statement.",
"Thank you for your help!",
]
response_index = 0
# Run with human-in-the-loop pattern
print("[AutoGen] Swarm handoff conversation:")
task_result = await Console(team.run_stream(task=scripted_responses[response_index]))
last_message = task_result.messages[-1]
response_index += 1
# Continue conversation when agents handoff to user
while (
isinstance(last_message, HandoffMessage)
and last_message.target == "user"
and response_index < len(scripted_responses)
):
user_message = scripted_responses[response_index]
task_result = await Console(
team.run_stream(task=HandoffMessage(source="user", target=last_message.source, content=user_message))
)
last_message = task_result.messages[-1]
response_index += 1
async def run_agent_framework() -> None:
"""Agent Framework's HandoffBuilder for agent coordination."""
from agent_framework import (
WorkflowRunState,
)
from agent_framework.openai import OpenAIChatClient
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
client = OpenAIChatClient(model="gpt-4.1-mini")
# Create triage agent
triage_agent = Agent(client=client,
name="triage",
instructions=(
"You are a triage agent. Analyze the user's request and route to the appropriate specialist:\n"
"- For billing issues: call handoff_to_billing_agent\n"
"- For technical issues: call handoff_to_technical_support"
),
description="Routes requests to appropriate specialists",
)
# Create billing specialist
billing_agent = Agent(client=client,
name="billing_agent",
instructions="You are a billing specialist. Help with payment and billing questions. Provide clear assistance.",
description="Handles billing and payment questions",
)
# Create technical support specialist
tech_support = Agent(client=client,
name="technical_support",
instructions="You are technical support. Help with technical issues. Provide clear assistance.",
description="Handles technical support questions",
)
# Create handoff workflow - simpler configuration
# After specialists respond, control returns to user (via triage as coordinator)
workflow = (
HandoffBuilder(
name="support_handoff",
participants=[triage_agent, billing_agent, tech_support],
termination_condition=lambda conv: sum(1 for msg in conv if msg.role == "user") > 3,
)
.with_start_agent(triage_agent)
.add_handoff(triage_agent, [billing_agent, tech_support])
.build()
)
# Scripted user responses
scripted_responses = [
"I was charged twice for my subscription",
"Yes, the charge of $49.99 appears twice on my credit card statement.",
"Thank you for your help!",
]
# Run with initial message
print("[Agent Framework] Handoff conversation:")
print("---------- user ----------")
print(scripted_responses[0])
current_executor = None
stream_line_open = False
pending_requests: list[WorkflowEvent] = []
async for event in workflow.run(scripted_responses[0], stream=True):
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
# Print executor name header when switching to a new agent
if current_executor != event.executor_id:
if stream_line_open:
print()
stream_line_open = False
print(f"---------- {event.executor_id} ----------")
current_executor = event.executor_id
stream_line_open = True
if event.data:
print(event.data.text, end="", flush=True)
elif event.type == "request_info":
if isinstance(event.data, HandoffAgentUserRequest):
pending_requests.append(event)
elif event.type == "status":
if event.state in {WorkflowRunState.IDLE_WITH_PENDING_REQUESTS} and stream_line_open:
print()
stream_line_open = False
# Process scripted responses
response_index = 1
while pending_requests and response_index < len(scripted_responses):
user_response = scripted_responses[response_index]
print("---------- user ----------")
print(user_response)
responses: dict[str, Any] = {
req.request_id: HandoffAgentUserRequest.create_response(user_response) for req in pending_requests
} # type: ignore
pending_requests = []
current_executor = None
stream_line_open = False
async for event in workflow.run(stream=True, responses=responses):
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
# Print executor name header when switching to a new agent
if current_executor != event.executor_id:
if stream_line_open:
print()
stream_line_open = False
print(f"---------- {event.executor_id} ----------")
current_executor = event.executor_id
stream_line_open = True
if event.data:
print(event.data.text, end="", flush=True)
elif event.type == "request_info":
if isinstance(event.data, HandoffAgentUserRequest):
pending_requests.append(event)
elif event.type == "status":
if (
event.state in {WorkflowRunState.IDLE_WITH_PENDING_REQUESTS, WorkflowRunState.IDLE}
and stream_line_open
):
print()
stream_line_open = False
response_index += 1
if stream_line_open:
print()
print() # Final newline after conversation
async def main() -> None:
print("=" * 60)
print("Swarm / Handoff Pattern Comparison")
print("=" * 60)
print("AutoGen: Swarm with handoffs")
print("Agent Framework: HandoffBuilder\n")
await run_autogen()
print()
await run_agent_framework()
if __name__ == "__main__":
asyncio.run(main())