Python: [BREAKING] Python: Intro group chat and refactor orchestrations. Fix as_agent(). Standardize orchestration start msg types. (#1538)

* Intro group chat and refactor magentic. Fix as_agent()

* Cleanup and improvements

* Add as_agent docstring clarification

* Standardize orchestration messages to use agent-style inputs.

* Simplify group chat constructs

* Further cleanup

* Add sk to af group chat migration sample. Update README.

* Improvements and simplifications

* consolidating shared orchestration logic

* Further clean up

* Add group chat sample

* Improve typing

* Fix test imports

* Fix readme links

* Cleanup per PR Feedback
This commit is contained in:
Evan Mattson
2025-10-25 09:14:06 +09:00
committed by GitHub
Unverified
parent 899d8ff775
commit e3aad8e4e0
38 changed files with 5024 additions and 814 deletions
@@ -1,10 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
# Semantic Kernel → Microsoft Agent Framework Migration Samples
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.
## Whats Included
## Whats Included
### 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.
@@ -32,7 +33,8 @@ This gallery helps Semantic Kernel (SK) developers move to the Microsoft Agent F
### 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.
- [group_chat.py](orchestrations/group_chat.py) — Group chat coordination with an LLM-backed manager in both SDKs.
- [handoff.py](orchestrations/handoff.py) - Handoff coordination between agents.
- [magentic.py](orchestrations/magentic.py) — Magentic Team orchestration vs. AF builder wiring.
### Processes
@@ -55,7 +57,7 @@ python samples/semantic-kernel-migration/chat_completion/01_basic_chat_completio
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 `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:
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:
```
cd samples/semantic-kernel-migration
uv venv --python 3.10 .venv-migration
@@ -0,0 +1,266 @@
# Copyright (c) Microsoft. All rights reserved.
"""Side-by-side group chat orchestrations for Agent Framework and Semantic Kernel."""
import asyncio
import sys
from collections.abc import Sequence
from typing import Any, cast
from agent_framework import ChatAgent, ChatMessage, GroupChatBuilder, WorkflowOutputEvent
from agent_framework.azure import AzureOpenAIChatClient, AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
from semantic_kernel.agents import Agent, ChatCompletionAgent, GroupChatOrchestration
from semantic_kernel.agents.orchestration.group_chat import (
BooleanResult,
GroupChatManager,
MessageResult,
StringResult,
)
from semantic_kernel.agents.runtime import InProcessRuntime
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.contents import AuthorRole, ChatHistory, ChatMessageContent
from semantic_kernel.functions import KernelArguments
from semantic_kernel.kernel import Kernel
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
DISCUSSION_TOPIC = "What are the essential steps for launching a community hackathon?"
######################################################################
# Semantic Kernel orchestration path
######################################################################
def build_semantic_kernel_agents() -> list[Agent]:
credential = AzureCliCredential()
researcher = ChatCompletionAgent(
name="Researcher",
description="Collects background information and potential resources.",
instructions=(
"Gather concise facts or considerations that help plan a community hackathon. "
"Keep your responses factual and scannable."
),
service=AzureChatCompletion(credential=credential),
)
planner = ChatCompletionAgent(
name="Planner",
description="Synthesizes an actionable plan from available notes.",
instructions=(
"Use the running conversation to draft a structured action plan. Emphasize logistics and sequencing."
),
service=AzureChatCompletion(credential=credential),
)
return [researcher, planner]
class ChatCompletionGroupChatManager(GroupChatManager):
"""Group chat manager that delegates orchestration decisions to an Azure OpenAI deployment."""
service: ChatCompletionClientBase
topic: str
termination_prompt: str = (
"You are coordinating a conversation about '{{topic}}'. "
"Decide if the discussion has produced a solid answer. "
'Respond using JSON: {"result": true|false, "reason": "..."}.'
)
selection_prompt: str = (
"You are coordinating a conversation about '{{topic}}'. "
"Choose the next participant by returning JSON with keys (result, reason). "
"The result must match one of: {{participants}}."
)
summary_prompt: str = (
"You have just finished a discussion about '{{topic}}'. "
"Summarize the plan and highlight key takeaways. Return JSON with keys (result, reason) where "
"result is the final response text."
)
def __init__(self, *, topic: str, service: ChatCompletionClientBase) -> None:
super().__init__(topic=topic, service=service)
self._round_robin_index = 0
async def _render_prompt(self, template: str, **kwargs: Any) -> str:
prompt_template = KernelPromptTemplate(prompt_template_config=PromptTemplateConfig(template=template))
return await prompt_template.render(Kernel(), arguments=KernelArguments(**kwargs))
@override
async def should_request_user_input(self, chat_history: ChatHistory) -> BooleanResult:
return BooleanResult(result=False, reason="This orchestration is fully automated.")
@override
async def should_terminate(self, chat_history: ChatHistory) -> BooleanResult:
rendered_prompt = await self._render_prompt(self.termination_prompt, topic=self.topic)
chat_history.messages.insert(
0,
ChatMessageContent(role=AuthorRole.SYSTEM, content=rendered_prompt),
)
chat_history.add_message(
ChatMessageContent(role=AuthorRole.USER, content="Decide if the discussion is complete."),
)
response = await self.service.get_chat_message_content(
chat_history,
settings=PromptExecutionSettings(response_format=BooleanResult),
)
result = BooleanResult.model_validate_json(response.content)
return result
@override
async def select_next_agent(
self,
chat_history: ChatHistory,
participant_descriptions: dict[str, str],
) -> StringResult:
rendered_prompt = await self._render_prompt(
self.selection_prompt,
topic=self.topic,
participants=", ".join(participant_descriptions.keys()),
)
chat_history.messages.insert(
0,
ChatMessageContent(role=AuthorRole.SYSTEM, content=rendered_prompt),
)
chat_history.add_message(
ChatMessageContent(role=AuthorRole.USER, content="Pick the next participant to speak."),
)
response = await self.service.get_chat_message_content(
chat_history,
settings=PromptExecutionSettings(response_format=StringResult),
)
result = StringResult.model_validate_json(response.content)
if result.result not in participant_descriptions:
raise RuntimeError(f"Unknown participant selected: {result.result}")
return result
@override
async def filter_results(self, chat_history: ChatHistory) -> MessageResult:
rendered_prompt = await self._render_prompt(self.summary_prompt, topic=self.topic)
chat_history.messages.insert(
0,
ChatMessageContent(role=AuthorRole.SYSTEM, content=rendered_prompt),
)
chat_history.add_message(
ChatMessageContent(role=AuthorRole.USER, content="Summarize the plan."),
)
response = await self.service.get_chat_message_content(
chat_history,
settings=PromptExecutionSettings(response_format=StringResult),
)
string_result = StringResult.model_validate_json(response.content)
return MessageResult(
result=ChatMessageContent(role=AuthorRole.ASSISTANT, content=string_result.result),
reason=string_result.reason,
)
async def sk_agent_response_callback(message: ChatMessageContent | Sequence[ChatMessageContent]) -> None:
if isinstance(message, ChatMessageContent):
messages: Sequence[ChatMessageContent] = [message]
elif isinstance(message, Sequence) and not isinstance(message, (str, bytes)):
messages = list(message)
else:
messages = [cast(ChatMessageContent, message)]
for item in messages:
print(f"# {item.name}\n{item.content}\n")
async def run_semantic_kernel_example(task: str) -> str:
credential = AzureCliCredential()
orchestration = GroupChatOrchestration(
members=build_semantic_kernel_agents(),
manager=ChatCompletionGroupChatManager(
topic=DISCUSSION_TOPIC,
service=AzureChatCompletion(credential=credential),
max_rounds=8,
),
agent_response_callback=sk_agent_response_callback,
)
runtime = InProcessRuntime()
runtime.start()
try:
orchestration_result = await orchestration.invoke(task=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
######################################################################
async def run_agent_framework_example(task: str) -> str:
credential = AzureCliCredential()
researcher = ChatAgent(
name="Researcher",
description="Collects background information and potential resources.",
instructions=(
"Gather concise facts or considerations that help plan a community hackathon. "
"Keep your responses factual and scannable."
),
chat_client=AzureOpenAIChatClient(credential=credential),
)
planner = ChatAgent(
name="Planner",
description="Turns the collected notes into a concrete action plan.",
instructions=("Propose a structured action plan that accounts for logistics, roles, and timeline."),
chat_client=AzureOpenAIResponsesClient(credential=credential),
)
workflow = (
GroupChatBuilder()
.set_prompt_based_manager(
chat_client=AzureOpenAIChatClient(credential=credential),
display_name="Coordinator",
)
.participants(researcher=researcher, planner=planner)
.build()
)
final_response = ""
async for event in workflow.run_stream(task):
if isinstance(event, WorkflowOutputEvent):
data = event.data
final_response = data.text or "" if isinstance(data, ChatMessage) else str(data)
return final_response
async def main() -> None:
task = "Kick off the group discussion."
print("===== Agent Framework Group Chat =====")
af_response = await run_agent_framework_example(task)
print(af_response or "No response returned.")
print()
print("===== Semantic Kernel Group Chat =====")
sk_response = await run_semantic_kernel_example(task)
print(sk_response or "No response returned.")
if __name__ == "__main__":
asyncio.run(main())
@@ -1,13 +1,10 @@
# 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 collections.abc import AsyncIterable, Iterator, Sequence
from typing import cast
from agent_framework import (
ChatMessage,
@@ -29,13 +26,12 @@ from semantic_kernel.contents import (
FunctionResultContent,
StreamingChatMessageContent,
)
from semantic_kernel.functions import KernelArguments, kernel_function
from semantic_kernel.prompt_template import KernelPromptTemplate, PromptTemplateConfig
from semantic_kernel.functions import kernel_function
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
pass # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
pass # pragma: no cover
CUSTOMER_PROMPT = "I need help with order 12345. I want a replacement and need to know when it will arrive."