Python: semantic-kernel to agent-framework migration code samples (#1045)

* wip migrations

* Wip: workflow migrations

* Add migration samples for sk to af

* Fix typo

* Fixes
This commit is contained in:
Evan Mattson
2025-10-01 16:02:03 +09:00
committed by GitHub
Unverified
parent 498fc06fd6
commit fb51d917fd
23 changed files with 1817 additions and 5 deletions
@@ -0,0 +1,252 @@
# Copyright (c) Microsoft. All rights reserved.
"""Side-by-side sample comparing Semantic Kernel Process Framework and Agent Framework workflows."""
import asyncio
import logging
from dataclasses import dataclass
from enum import Enum
from typing import TYPE_CHECKING, ClassVar, cast
######################################################################
# region Agent Framework imports
######################################################################
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, WorkflowOutputEvent, handler
from pydantic import BaseModel, Field
######################################################################
# region Semantic Kernel imports
######################################################################
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
from semantic_kernel.functions import kernel_function
from semantic_kernel.processes.kernel_process.kernel_process_event import KernelProcessEvent
from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep
from semantic_kernel.processes.kernel_process.kernel_process_step_context import KernelProcessStepContext
from semantic_kernel.processes.kernel_process.kernel_process_step_state import KernelProcessStepState
from semantic_kernel.processes.process_builder import ProcessBuilder
if TYPE_CHECKING:
from semantic_kernel.processes.kernel_process import KernelProcess
from semantic_kernel.processes.local_runtime.local_kernel_process import LocalKernelProcessContext
async def _start_local_kernel_process(
*,
process: "KernelProcess",
kernel: Kernel,
initial_event: KernelProcessEvent | str | Enum,
**kwargs: object,
) -> "LocalKernelProcessContext":
from semantic_kernel.processes.local_runtime.local_kernel_process import start as start_local_kernel_process
return await start_local_kernel_process(
process=process,
kernel=kernel,
initial_event=initial_event,
**kwargs,
)
logging.basicConfig(level=logging.WARNING)
class CommonEvents(Enum):
"""Common events for both samples."""
USER_INPUT_RECEIVED = "UserInputReceived"
COMPLETION_RESPONSE_GENERATED = "CompletionResponseGenerated"
WELCOME_DONE = "WelcomeDone"
A_STEP_DONE = "AStepDone"
B_STEP_DONE = "BStepDone"
C_STEP_DONE = "CStepDone"
START_A_REQUESTED = "StartARequested"
START_B_REQUESTED = "StartBRequested"
EXIT_REQUESTED = "ExitRequested"
START_PROCESS = "StartProcess"
######################################################################
# region Semantic Kernel Process Framework path
######################################################################
class KickOffStep(KernelProcessStep[None]):
KICK_OFF_FUNCTION: ClassVar[str] = "kick_off"
@kernel_function(name=KICK_OFF_FUNCTION)
async def print_welcome_message(self, context: KernelProcessStepContext):
await context.emit_event(process_event=CommonEvents.START_A_REQUESTED, data="Get Going A")
await context.emit_event(process_event=CommonEvents.START_B_REQUESTED, data="Get Going B")
class AStep(KernelProcessStep[None]):
@kernel_function()
async def do_it(self, context: KernelProcessStepContext):
await asyncio.sleep(1)
await context.emit_event(process_event=CommonEvents.A_STEP_DONE.value, data="I did A")
class BStep(KernelProcessStep[None]):
@kernel_function()
async def do_it(self, context: KernelProcessStepContext):
await asyncio.sleep(2)
await context.emit_event(process_event=CommonEvents.B_STEP_DONE.value, data="I did B")
class CStepState(BaseModel):
current_cycle: int = 0
class CStep(KernelProcessStep[CStepState]):
state: CStepState = Field(default_factory=CStepState)
async def activate(self, state: KernelProcessStepState[CStepState]):
self.state = state.state
@kernel_function()
async def do_it(self, context: KernelProcessStepContext, astepdata: str, bstepdata: str):
self.state.current_cycle += 1
print(f"CStep Current Cycle: {self.state.current_cycle}")
if self.state.current_cycle == 3:
print("CStep Exit Requested")
await context.emit_event(process_event=CommonEvents.EXIT_REQUESTED.value)
return
await context.emit_event(process_event=CommonEvents.C_STEP_DONE.value)
kernel = Kernel()
async def run_semantic_kernel_process_example() -> None:
kernel.add_service(OpenAIChatCompletion(service_id="default"))
process = ProcessBuilder(name="Process Framework Sample")
kickoff_step = process.add_step(step_type=KickOffStep)
step_a = process.add_step(step_type=AStep)
step_b = process.add_step(step_type=BStep)
step_c = process.add_step(step_type=CStep)
process.on_input_event(event_id=CommonEvents.START_PROCESS.value).send_event_to(target=kickoff_step)
kickoff_step.on_event(event_id=CommonEvents.START_A_REQUESTED.value).send_event_to(target=step_a)
kickoff_step.on_event(event_id=CommonEvents.START_B_REQUESTED.value).send_event_to(target=step_b)
step_a.on_event(event_id=CommonEvents.A_STEP_DONE.value).send_event_to(target=step_c, parameter_name="astepdata")
step_b.on_event(event_id=CommonEvents.B_STEP_DONE.value).send_event_to(target=step_c, parameter_name="bstepdata")
step_c.on_event(event_id=CommonEvents.C_STEP_DONE.value).send_event_to(target=kickoff_step)
step_c.on_event(event_id=CommonEvents.EXIT_REQUESTED.value).stop_process()
kernel_process: "KernelProcess" = process.build()
async with await _start_local_kernel_process(
process=kernel_process,
kernel=kernel,
initial_event=KernelProcessEvent(id=CommonEvents.START_PROCESS.value, data="Initial"),
) as process_context:
process_state = await process_context.get_state()
c_step_state: KernelProcessStepState[CStepState] | None = next(
(s.state for s in process_state.steps if s.state.name == "CStep"),
None,
)
if c_step_state is None or c_step_state.state is None:
raise RuntimeError("CStep state unavailable")
assert c_step_state.state.current_cycle == 3 # nosec
print(f"Final State Check: CStepState current cycle: {c_step_state.state.current_cycle}")
######################################################################
# region Agent Framework workflow path
######################################################################
@dataclass
class StepResult:
origin: str
cycle: int
data: str
class KickOffExecutor(Executor):
def __init__(self, *, id: str = "kickoff") -> None:
super().__init__(id=id)
self._next_cycle = 0
@handler
async def handle(self, event: CommonEvents, ctx: WorkflowContext[int]) -> None:
if event not in {CommonEvents.START_PROCESS, CommonEvents.C_STEP_DONE}:
return
self._next_cycle += 1
await ctx.send_message(self._next_cycle)
class DelayedStepExecutor(Executor):
def __init__(self, *, name: str, delay_seconds: float) -> None:
super().__init__(id=name)
self._delay = delay_seconds
self._name = name
@handler
async def handle(self, cycle: int, ctx: WorkflowContext[StepResult]) -> None:
await asyncio.sleep(self._delay)
await ctx.send_message(StepResult(origin=self._name, cycle=cycle, data=f"I did {self._name.upper()[-1]}"))
class FanInExecutor(Executor):
def __init__(self, *, required_cycles: int = 3, id: str = "fanin") -> None:
super().__init__(id=id)
self._completed_cycles = 0
self._required_cycles = required_cycles
@handler
async def handle(self, results: list[StepResult], ctx: WorkflowContext[CommonEvents, str]) -> None:
if not results:
return
cycle_number = results[0].cycle
summary = ", ".join(f"{r.origin}: {r.data}" for r in results)
print(f"Cycle {cycle_number} aggregate -> {summary}")
self._completed_cycles += 1
if self._completed_cycles >= self._required_cycles:
await ctx.yield_output(f"Completed {self._completed_cycles} cycles")
return
await ctx.send_message(CommonEvents.C_STEP_DONE)
async def run_agent_framework_workflow_example() -> str | None:
kickoff = KickOffExecutor()
step_a = DelayedStepExecutor(name="step_a", delay_seconds=1)
step_b = DelayedStepExecutor(name="step_b", delay_seconds=2)
aggregate = FanInExecutor(required_cycles=3)
workflow = (
WorkflowBuilder()
.add_edge(kickoff, step_a)
.add_edge(kickoff, step_b)
.add_fan_in_edges([step_a, step_b], aggregate)
.add_edge(aggregate, kickoff)
.set_start_executor(kickoff)
.build()
)
final_text: str | None = None
async for event in workflow.run_stream(CommonEvents.START_PROCESS):
if isinstance(event, WorkflowOutputEvent):
final_text = cast(str, event.data)
return final_text
async def main() -> None:
print("===== Agent Framework Workflow =====")
af_result = await run_agent_framework_workflow_example()
if af_result:
print(af_result)
else:
print("No Agent Framework output.")
print("===== Semantic Kernel Process Framework =====")
await run_semantic_kernel_process_example()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,279 @@
# Copyright (c) Microsoft. All rights reserved.
"""Nested process comparison between Semantic Kernel Process Framework and Agent Framework sub-workflows."""
import asyncio
import logging
from collections.abc import Sequence
from dataclasses import dataclass
from enum import Enum
from typing import ClassVar, cast
######################################################################
# region Agent Framework imports
######################################################################
from agent_framework import (
Executor,
WorkflowBuilder,
WorkflowContext,
WorkflowExecutor,
WorkflowOutputEvent,
handler,
)
from pydantic import BaseModel, Field
######################################################################
# region Semantic Kernel imports
######################################################################
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
from semantic_kernel.functions import kernel_function
from semantic_kernel.processes.kernel_process.kernel_process import KernelProcess
from semantic_kernel.processes.kernel_process.kernel_process_event import KernelProcessEventVisibility
from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep
from semantic_kernel.processes.kernel_process.kernel_process_step_context import KernelProcessStepContext
from semantic_kernel.processes.kernel_process.kernel_process_step_state import KernelProcessStepState
from semantic_kernel.processes.local_runtime.local_kernel_process import start
from semantic_kernel.processes.process_builder import ProcessBuilder
from typing_extensions import Never
######################################################################
# endregion
######################################################################
logging.basicConfig(level=logging.WARNING)
class ProcessEvents(Enum):
START_PROCESS = "StartProcess"
START_INNER_PROCESS = "StartInnerProcess"
OUTPUT_READY_PUBLIC = "OutputReadyPublic"
OUTPUT_READY_INTERNAL = "OutputReadyInternal"
######################################################################
# region Semantic Kernel nested process path
######################################################################
class StepState(BaseModel):
last_message: str | None = None
class EchoStep(KernelProcessStep[None]):
ECHO: ClassVar[str] = "echo"
@kernel_function(name=ECHO)
async def echo(self, message: str) -> str:
print(f"[ECHO] {message}")
return message
class RepeatStep(KernelProcessStep[StepState]):
REPEAT: ClassVar[str] = "repeat"
state: StepState = Field(default_factory=StepState)
async def activate(self, state: KernelProcessStepState[StepState]):
self.state = state.state
@kernel_function(name=REPEAT)
async def repeat(
self,
message: str,
context: KernelProcessStepContext,
count: int = 2,
) -> None:
output = " ".join([message] * count)
self.state.last_message = output
print(f"[REPEAT] {output}")
await context.emit_event(
process_event=ProcessEvents.OUTPUT_READY_PUBLIC.value,
data=output,
visibility=KernelProcessEventVisibility.Public,
)
await context.emit_event(
process_event=ProcessEvents.OUTPUT_READY_INTERNAL.value,
data=output,
visibility=KernelProcessEventVisibility.Internal,
)
def _create_linear_process(name: str) -> ProcessBuilder:
process_builder = ProcessBuilder(name=name)
echo_step = process_builder.add_step(step_type=EchoStep)
repeat_step = process_builder.add_step(step_type=RepeatStep)
process_builder.on_input_event(event_id=ProcessEvents.START_PROCESS.value).send_event_to(target=echo_step)
echo_step.on_function_result(function_name=EchoStep.ECHO).send_event_to(
target=repeat_step,
parameter_name="message",
)
return process_builder
_semantic_kernel = Kernel()
async def run_semantic_kernel_nested_process() -> None:
_semantic_kernel.add_service(OpenAIChatCompletion(service_id="default"))
process_builder = _create_linear_process("Outer")
nested_process_step = process_builder.add_step_from_process(_create_linear_process("Inner"))
process_builder.steps[1].on_event(ProcessEvents.OUTPUT_READY_INTERNAL.value).send_event_to(
nested_process_step.where_input_event_is(ProcessEvents.START_PROCESS.value)
)
kernel_process = process_builder.build()
process_handle = await start(
process=kernel_process,
kernel=_semantic_kernel,
initial_event=ProcessEvents.START_PROCESS.value,
data="Test",
)
process_info = await process_handle.get_state()
inner_process: KernelProcess | None = next(
(s for s in process_info.steps if s.state.name == "Inner"),
None,
)
if inner_process is None:
raise RuntimeError("Inner process state missing")
repeat_state: KernelProcessStepState[StepState] | None = next(
(s.state for s in inner_process.steps if s.state.name == "RepeatStep"),
None,
)
if repeat_state is None or repeat_state.state is None:
raise RuntimeError("RepeatStep state missing")
assert repeat_state.state.last_message == "Test Test Test Test" # nosec
######################################################################
# region Agent Framework nested workflow path
######################################################################
@dataclass
class RepeatPayload:
message: str
count: int = 2
class KickoffExecutor(Executor):
def __init__(self) -> None:
super().__init__(id="kickoff")
@handler
async def start(self, message: str, ctx: WorkflowContext[RepeatPayload]) -> None:
print(f"[OUTER] Start with message: {message}")
await ctx.send_message(RepeatPayload(message=message, count=2))
class OuterEchoExecutor(Executor):
def __init__(self) -> None:
super().__init__(id="outer_echo")
@handler
async def echo(self, payload: RepeatPayload, ctx: WorkflowContext[RepeatPayload]) -> None:
print(f"[OUTER ECHO] {payload.message}")
await ctx.send_message(payload)
class OuterRepeatExecutor(Executor):
def __init__(self, *, inner_target_id: str) -> None:
super().__init__(id="outer_repeat")
self._inner_target_id = inner_target_id
@handler
async def repeat(self, payload: RepeatPayload, ctx: WorkflowContext[RepeatPayload]) -> None:
repeated = " ".join([payload.message] * payload.count)
print(f"[OUTER REPEAT] {repeated}")
await ctx.send_message(RepeatPayload(message=repeated, count=2), target_id=self._inner_target_id)
class InnerEchoExecutor(Executor):
def __init__(self) -> None:
super().__init__(id="inner_echo")
@handler
async def echo(self, payload: RepeatPayload, ctx: WorkflowContext[RepeatPayload]) -> None:
print(f" [INNER ECHO] {payload.message}")
await ctx.send_message(payload)
class InnerRepeatExecutor(Executor):
def __init__(self) -> None:
super().__init__(id="inner_repeat")
@handler
async def repeat(self, payload: RepeatPayload, ctx: WorkflowContext[Never, str]) -> None:
repeated = " ".join([payload.message] * payload.count)
print(f" [INNER REPEAT] {repeated}")
await ctx.yield_output(repeated)
class CollectResultExecutor(Executor):
def __init__(self) -> None:
super().__init__(id="collector")
@handler
async def collect(self, result: str, ctx: WorkflowContext[Never, str]) -> None:
print(f"[COLLECTOR] Final result -> {result}")
await ctx.yield_output(result)
def _build_inner_workflow() -> WorkflowExecutor:
inner_echo = InnerEchoExecutor()
inner_repeat = InnerRepeatExecutor()
inner_workflow = WorkflowBuilder().set_start_executor(inner_echo).add_edge(inner_echo, inner_repeat).build()
return WorkflowExecutor(inner_workflow, id="inner_workflow")
async def run_agent_framework_nested_workflow(initial_message: str) -> Sequence[str]:
inner_executor = _build_inner_workflow()
kickoff = KickoffExecutor()
outer_echo = OuterEchoExecutor()
outer_repeat = OuterRepeatExecutor(inner_target_id=inner_executor.id)
collector = CollectResultExecutor()
outer_workflow = (
WorkflowBuilder()
.set_start_executor(kickoff)
.add_edge(kickoff, outer_echo)
.add_edge(outer_echo, outer_repeat)
.add_edge(outer_repeat, inner_executor)
.add_edge(inner_executor, collector)
.build()
)
results: list[str] = []
async for event in outer_workflow.run_stream(initial_message):
if isinstance(event, WorkflowOutputEvent):
results.append(cast(str, event.data))
return results
######################################################################
# endregion
######################################################################
async def main() -> None:
print("===== Agent Framework Nested Workflow =====")
af_results = await run_agent_framework_nested_workflow("Test")
for index, value in enumerate(af_results, start=1):
print(f"Result {index}: {value}")
print("\n===== Semantic Kernel Nested Process =====")
await run_semantic_kernel_nested_process()
if __name__ == "__main__":
asyncio.run(main())