mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Add Handoff orchestration pattern support (#1469)
* Add Handoff orchestration pattern support * PR feedback * Use AOAI client in samples * Adjust to tool * Handoff to sub-agent via ai function * PR feedback * More cleanup * Improvements * PR feedback cleanup * Add handoff migration sample. * Remove type ignore * fix markdown link formatting * Remove readme link for non-existent sample
This commit is contained in:
committed by
GitHub
Unverified
parent
4554de00ab
commit
b66619a544
@@ -4,13 +4,40 @@
|
||||
This gallery helps Semantic Kernel (SK) developers move to the Microsoft Agent Framework (AF) with minimal guesswork. Each script pairs SK code with its AF equivalent so you can compare primitives, tooling, and orchestration patterns side by side while you migrate production workloads.
|
||||
|
||||
## What’s Included
|
||||
- `chat_completion/` – SK `ChatCompletionAgent` scenarios and their AF `ChatAgent` counterparts (basic chat, tooling, threading/streaming).
|
||||
- `azure_ai_agent/` – Remote Azure AI agent examples, including hosted code interpreter and explicit thread reuse.
|
||||
- `openai_assistant/` – Assistants API migrations covering basic usage, code interpreter, and custom function tools.
|
||||
- `openai_responses/` – Responses API parity samples with tooling and structured JSON output.
|
||||
- `copilot_studio/` – Copilot Studio agent parity, tools, and streaming examples.
|
||||
- `orchestrations/` – Sequential, Concurrent, and Magentic workflow migrations that mirror SK Team abstractions.
|
||||
- `processes/` – Fan-out/fan-in and nested process examples that contrast SK’s Process Framework with AF workflows.
|
||||
|
||||
### Chat completion parity
|
||||
- [01_basic_chat_completion.py](chat_completion/01_basic_chat_completion.py) — Minimal SK `ChatCompletionAgent` and AF `ChatAgent` conversation.
|
||||
- [02_chat_completion_with_tool.py](chat_completion/02_chat_completion_with_tool.py) — Adds a simple tool/function call in both SDKs.
|
||||
- [03_chat_completion_thread_and_stream.py](chat_completion/03_chat_completion_thread_and_stream.py) — Demonstrates thread reuse and streaming prompts.
|
||||
|
||||
### Azure AI agent parity
|
||||
- [01_basic_azure_ai_agent.py](azure_ai_agent/01_basic_azure_ai_agent.py) — Create and run an Azure AI agent end to end.
|
||||
- [02_azure_ai_agent_with_code_interpreter.py](azure_ai_agent/02_azure_ai_agent_with_code_interpreter.py) — Enable hosted code interpreter/tool execution.
|
||||
- [03_azure_ai_agent_threads_and_followups.py](azure_ai_agent/03_azure_ai_agent_threads_and_followups.py) — Persist threads and follow-ups across invocations.
|
||||
|
||||
### OpenAI Assistants API parity
|
||||
- [01_basic_openai_assistant.py](openai_assistant/01_basic_openai_assistant.py) — Baseline assistant comparison.
|
||||
- [02_openai_assistant_with_code_interpreter.py](openai_assistant/02_openai_assistant_with_code_interpreter.py) — Code interpreter tool usage.
|
||||
- [03_openai_assistant_function_tool.py](openai_assistant/03_openai_assistant_function_tool.py) — Custom function tooling.
|
||||
|
||||
### OpenAI Responses API parity
|
||||
- [01_basic_responses_agent.py](openai_responses/01_basic_responses_agent.py) — Basic responses agent migration.
|
||||
- [02_responses_agent_with_tool.py](openai_responses/02_responses_agent_with_tool.py) — Tool-augmented responses workflows.
|
||||
- [03_responses_agent_structured_output.py](openai_responses/03_responses_agent_structured_output.py) — Structured JSON output alignment.
|
||||
|
||||
### Copilot Studio parity
|
||||
- [01_basic_copilot_studio_agent.py](copilot_studio/01_basic_copilot_studio_agent.py) — Minimal Copilot Studio agent invocation.
|
||||
- [02_copilot_studio_streaming.py](copilot_studio/02_copilot_studio_streaming.py) — Streaming responses from Copilot Studio agents.
|
||||
|
||||
### Orchestrations
|
||||
- [sequential.py](orchestrations/sequential.py) — Step-by-step SK Team → AF `SequentialBuilder` migration.
|
||||
- [concurrent_basic.py](orchestrations/concurrent_basic.py) — Concurrent orchestration parity.
|
||||
- [handoff.py](orchestrations/handoff.py) — Support triage handoff migration with specialist routing.
|
||||
- [magentic.py](orchestrations/magentic.py) — Magentic Team orchestration vs. AF builder wiring.
|
||||
|
||||
### Processes
|
||||
- [fan_out_fan_in_process.py](processes/fan_out_fan_in_process.py) — Fan-out/fan-in comparison between SK Process Framework and AF workflows.
|
||||
- [nested_process.py](processes/nested_process.py) — Nested process orchestration vs. AF sub-workflows.
|
||||
|
||||
Each script is fully async and the `main()` routine runs both implementations back to back so you can observe their outputs in a single execution.
|
||||
|
||||
@@ -23,14 +50,14 @@ Each script is fully async and the `main()` routine runs both implementations ba
|
||||
## Running Single-Agent Samples
|
||||
From the repository root:
|
||||
```
|
||||
python samantic-kernel-migration/chat_completion/01_basic_chat_completion.py
|
||||
python samples/semantic-kernel-migration/chat_completion/01_basic_chat_completion.py
|
||||
```
|
||||
Every script accepts no CLI arguments and will first call the SK implementation, followed by the AF version. Adjust the prompt or credentials inside the file as necessary before running.
|
||||
|
||||
## Running Orchestration & Workflow Samples
|
||||
Advanced comparisons are split between `samantic-kernel-migration/orchestrations` (Sequential, Concurrent, Magentic) and `samantic-kernel-migration/processes` (fan-out/fan-in, nested). You can run them directly, or isolate dependencies in a throwaway virtual environment:
|
||||
Advanced comparisons are split between `samples/semantic-kernel-migration/orchestrations` (Sequential, Concurrent, Group Chat, Handoff, Magentic) and `samples/semantic-kernel-migration/processes` (fan-out/fan-in, nested). You can run them directly, or isolate dependencies in a throwaway virtual environment:
|
||||
```
|
||||
cd samantic-kernel-migration
|
||||
cd samples/semantic-kernel-migration
|
||||
uv venv --python 3.10 .venv-migration
|
||||
source .venv-migration/bin/activate
|
||||
uv pip install semantic-kernel agent-framework
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
"""Side-by-side handoff orchestrations for Semantic Kernel and Agent Framework."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from collections.abc import AsyncIterable, Sequence
|
||||
from typing import Any, cast
|
||||
from collections.abc import Iterator
|
||||
|
||||
from agent_framework import (
|
||||
ChatMessage,
|
||||
HandoffBuilder,
|
||||
HandoffUserInputRequest,
|
||||
RequestInfoEvent,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from semantic_kernel.agents import Agent, ChatCompletionAgent, HandoffOrchestration, OrchestrationHandoffs
|
||||
from semantic_kernel.agents.runtime import InProcessRuntime
|
||||
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
|
||||
from semantic_kernel.contents import (
|
||||
AuthorRole,
|
||||
ChatMessageContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
StreamingChatMessageContent,
|
||||
)
|
||||
from semantic_kernel.functions import KernelArguments, kernel_function
|
||||
from semantic_kernel.prompt_template import KernelPromptTemplate, PromptTemplateConfig
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import override # pragma: no cover
|
||||
|
||||
|
||||
CUSTOMER_PROMPT = "I need help with order 12345. I want a replacement and need to know when it will arrive."
|
||||
SCRIPTED_RESPONSES = [
|
||||
"The item arrived damaged. I'd like a replacement shipped to the same address.",
|
||||
"Great! Can you confirm the shipping cost won't be charged again?",
|
||||
"Thanks for confirming!",
|
||||
]
|
||||
|
||||
|
||||
######################################################################
|
||||
# Semantic Kernel orchestration path
|
||||
######################################################################
|
||||
|
||||
|
||||
class OrderStatusPlugin:
|
||||
@kernel_function
|
||||
def check_order_status(self, order_id: str) -> str:
|
||||
return f"Order {order_id} is shipped and will arrive in 2-3 days."
|
||||
|
||||
|
||||
class OrderRefundPlugin:
|
||||
@kernel_function
|
||||
def process_refund(self, order_id: str, reason: str) -> str:
|
||||
return f"Refund for order {order_id} has been processed successfully (reason: {reason})."
|
||||
|
||||
|
||||
class OrderReturnPlugin:
|
||||
@kernel_function
|
||||
def process_return(self, order_id: str, reason: str) -> str:
|
||||
return f"Return for order {order_id} has been processed successfully (reason: {reason})."
|
||||
|
||||
|
||||
def build_semantic_kernel_agents() -> tuple[list[Agent], OrchestrationHandoffs]:
|
||||
credential = AzureCliCredential()
|
||||
|
||||
triage = ChatCompletionAgent(
|
||||
name="TriageAgent",
|
||||
description="Customer support triage specialist.",
|
||||
instructions="Greet the customer, collect intent, and hand off to the right specialist.",
|
||||
service=AzureChatCompletion(credential=credential),
|
||||
)
|
||||
refund = ChatCompletionAgent(
|
||||
name="RefundAgent",
|
||||
description="Handles refunds.",
|
||||
instructions="Process refund requests.",
|
||||
service=AzureChatCompletion(credential=credential),
|
||||
plugins=[OrderRefundPlugin()],
|
||||
)
|
||||
order_status = ChatCompletionAgent(
|
||||
name="OrderStatusAgent",
|
||||
description="Looks up order status.",
|
||||
instructions="Provide shipping timelines and tracking information.",
|
||||
service=AzureChatCompletion(credential=credential),
|
||||
plugins=[OrderStatusPlugin()],
|
||||
)
|
||||
order_return = ChatCompletionAgent(
|
||||
name="OrderReturnAgent",
|
||||
description="Handles returns.",
|
||||
instructions="Coordinate order returns.",
|
||||
service=AzureChatCompletion(credential=credential),
|
||||
plugins=[OrderReturnPlugin()],
|
||||
)
|
||||
|
||||
handoffs = (
|
||||
OrchestrationHandoffs()
|
||||
.add_many(
|
||||
source_agent=triage.name,
|
||||
target_agents={
|
||||
refund.name: "Route refund-related requests here.",
|
||||
order_status.name: "Route shipping questions here.",
|
||||
order_return.name: "Route return-related requests here.",
|
||||
},
|
||||
)
|
||||
.add(refund.name, triage.name, "Return to triage for non-refund issues.")
|
||||
.add(order_status.name, triage.name, "Return to triage for non-status issues.")
|
||||
.add(order_return.name, triage.name, "Return to triage for non-return issues.")
|
||||
)
|
||||
|
||||
return [triage, refund, order_status, order_return], handoffs
|
||||
|
||||
|
||||
_sk_new_message = True
|
||||
|
||||
|
||||
def _sk_streaming_callback(message: StreamingChatMessageContent, is_final: bool) -> None:
|
||||
"""Display SK agent messages as they stream."""
|
||||
global _sk_new_message
|
||||
if _sk_new_message:
|
||||
print(f"{message.name}: ", end="", flush=True)
|
||||
_sk_new_message = False
|
||||
|
||||
if message.content:
|
||||
print(message.content, end="", flush=True)
|
||||
|
||||
for item in message.items:
|
||||
if isinstance(item, FunctionCallContent):
|
||||
print(f"[tool call: {item.name}({item.arguments})]", end="", flush=True)
|
||||
if isinstance(item, FunctionResultContent):
|
||||
print(f"[tool result: {item.result}]", end="", flush=True)
|
||||
|
||||
if is_final:
|
||||
print()
|
||||
_sk_new_message = True
|
||||
|
||||
|
||||
def _make_sk_human_responder(script: Iterator[str]) -> callable:
|
||||
def _responder() -> ChatMessageContent:
|
||||
try:
|
||||
user_text = next(script)
|
||||
except StopIteration:
|
||||
user_text = "Thanks, that's all."
|
||||
print(f"[User]: {user_text}")
|
||||
return ChatMessageContent(role=AuthorRole.USER, content=user_text)
|
||||
|
||||
return _responder
|
||||
|
||||
|
||||
async def run_semantic_kernel_example(initial_task: str, scripted_responses: Sequence[str]) -> str:
|
||||
agents, handoffs = build_semantic_kernel_agents()
|
||||
response_iter = iter(scripted_responses)
|
||||
|
||||
orchestration = HandoffOrchestration(
|
||||
members=agents,
|
||||
handoffs=handoffs,
|
||||
streaming_agent_response_callback=_sk_streaming_callback,
|
||||
human_response_function=_make_sk_human_responder(response_iter),
|
||||
)
|
||||
|
||||
runtime = InProcessRuntime()
|
||||
runtime.start()
|
||||
|
||||
try:
|
||||
orchestration_result = await orchestration.invoke(task=initial_task, runtime=runtime)
|
||||
final_message = await orchestration_result.get(timeout=30)
|
||||
if isinstance(final_message, ChatMessageContent):
|
||||
return final_message.content or ""
|
||||
return str(final_message)
|
||||
finally:
|
||||
await runtime.stop_when_idle()
|
||||
|
||||
|
||||
######################################################################
|
||||
# Agent Framework orchestration path
|
||||
######################################################################
|
||||
|
||||
|
||||
def _create_af_agents(client: AzureOpenAIChatClient):
|
||||
triage = client.create_agent(
|
||||
name="triage_agent",
|
||||
instructions=(
|
||||
"You are a customer support triage agent. Route requests:\n"
|
||||
"- handoff_to_refund_agent for refunds\n"
|
||||
"- handoff_to_order_status_agent for shipping/timeline questions\n"
|
||||
"- handoff_to_order_return_agent for returns"
|
||||
),
|
||||
)
|
||||
refund = client.create_agent(
|
||||
name="refund_agent",
|
||||
instructions=(
|
||||
"Handle refunds. Ask for order id and reason. If shipping info is needed, hand off to order_status_agent."
|
||||
),
|
||||
)
|
||||
status = client.create_agent(
|
||||
name="order_status_agent",
|
||||
instructions=(
|
||||
"Provide order status, tracking, and timelines. If billing questions appear, hand off to refund_agent."
|
||||
),
|
||||
)
|
||||
returns = client.create_agent(
|
||||
name="order_return_agent",
|
||||
instructions=(
|
||||
"Coordinate returns, confirm addresses, and summarize next steps. Hand off to triage_agent if unsure."
|
||||
),
|
||||
)
|
||||
return triage, refund, status, returns
|
||||
|
||||
|
||||
async def _drain_events(stream: AsyncIterable[WorkflowEvent]) -> list[WorkflowEvent]:
|
||||
return [event async for event in stream]
|
||||
|
||||
|
||||
def _collect_handoff_requests(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:
|
||||
requests: list[RequestInfoEvent] = []
|
||||
for event in events:
|
||||
if isinstance(event, RequestInfoEvent) and isinstance(event.data, HandoffUserInputRequest):
|
||||
requests.append(event)
|
||||
return requests
|
||||
|
||||
|
||||
def _extract_final_conversation(events: list[WorkflowEvent]) -> list[ChatMessage]:
|
||||
for event in events:
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
data = cast(list[ChatMessage], event.data)
|
||||
return data
|
||||
return []
|
||||
|
||||
|
||||
async def run_agent_framework_example(initial_task: str, scripted_responses: Sequence[str]) -> str:
|
||||
client = AzureOpenAIChatClient(credential=AzureCliCredential())
|
||||
triage, refund, status, returns = _create_af_agents(client)
|
||||
|
||||
workflow = (
|
||||
HandoffBuilder(name="sk_af_handoff_migration", participants=[triage, refund, status, returns])
|
||||
.set_coordinator(triage)
|
||||
.add_handoff(triage, [refund, status, returns])
|
||||
.add_handoff(refund, [status, triage])
|
||||
.add_handoff(status, [refund, triage])
|
||||
.add_handoff(returns, triage)
|
||||
.build()
|
||||
)
|
||||
|
||||
events = await _drain_events(workflow.run_stream(initial_task))
|
||||
pending = _collect_handoff_requests(events)
|
||||
scripted_iter = iter(scripted_responses)
|
||||
|
||||
final_events = events
|
||||
while pending:
|
||||
try:
|
||||
user_reply = next(scripted_iter)
|
||||
except StopIteration:
|
||||
user_reply = "Thanks, that's all."
|
||||
responses = {request.request_id: user_reply for request in pending}
|
||||
final_events = await _drain_events(workflow.send_responses_streaming(responses))
|
||||
pending = _collect_handoff_requests(final_events)
|
||||
|
||||
conversation = _extract_final_conversation(final_events)
|
||||
if not conversation:
|
||||
return ""
|
||||
|
||||
# Render final transcript succinctly.
|
||||
lines = []
|
||||
for message in conversation:
|
||||
text = message.text or ""
|
||||
if not text.strip():
|
||||
continue
|
||||
speaker = message.author_name or message.role.value
|
||||
lines.append(f"{speaker}: {text}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
######################################################################
|
||||
# Console entry point
|
||||
######################################################################
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
print("===== Agent Framework Handoff =====")
|
||||
af_transcript = await run_agent_framework_example(CUSTOMER_PROMPT, SCRIPTED_RESPONSES)
|
||||
print(af_transcript or "No output produced.")
|
||||
print()
|
||||
|
||||
print("===== Semantic Kernel Handoff =====")
|
||||
sk_result = await run_semantic_kernel_example(CUSTOMER_PROMPT, SCRIPTED_RESPONSES)
|
||||
print(sk_result or "No output produced.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user