Files
microsoft--semantic-kernel/dotnet/samples/GettingStartedWithProcesses/Step04/Step04_AgentOrchestration.cs
T
wehub-resource-sync b957a53def
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:21:23 +08:00

447 lines
18 KiB
C#

// Copyright (c) Microsoft. All rights reserved.
using System.ClientModel;
using Azure.Identity;
using Events;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.Chat;
using Microsoft.SemanticKernel.Agents.OpenAI;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using OpenAI;
using SharedSteps;
using Step04.Plugins;
using Step04.Steps;
namespace Step04;
/// <summary>
/// Demonstrate creation of a <see cref="KernelProcess"/> that orchestrates an <see cref="Agent"/> conversation.
/// For visual reference of the process check <see href="https://github.com/microsoft/semantic-kernel/tree/main/dotnet/samples/GettingStartedWithProcesses/README.md#step04_agentorchestration" >diagram</see>.
/// </summary>
public class Step04_AgentOrchestration : BaseTest
{
public Step04_AgentOrchestration(ITestOutputHelper output) : base(output, redirectSystemConsoleOutput: true)
{
this.Client =
this.UseOpenAIConfig ?
OpenAIAssistantAgent.CreateOpenAIClient(new ApiKeyCredential(this.ApiKey ?? throw new ConfigurationNotFoundException("OpenAI:ApiKey"))) :
!string.IsNullOrWhiteSpace(this.ApiKey) ?
OpenAIAssistantAgent.CreateAzureOpenAIClient(new ApiKeyCredential(this.ApiKey), new Uri(this.Endpoint!)) :
OpenAIAssistantAgent.CreateAzureOpenAIClient(new AzureCliCredential(), new Uri(this.Endpoint!));
}
protected OpenAIClient Client { get; init; }
// Target Open AI Services
protected override bool ForceOpenAI => true;
/// <summary>
/// Orchestrates a single agent gathering user input and then delegating to a group of agents.
/// The group of agents provide a response back to the single agent who continues to
/// interact with the user.
/// </summary>
[Fact]
public async Task DelegatedGroupChatAsync()
{
// Define process
KernelProcess process = SetupAgentProcess<BasicAgentChatUserInput>(nameof(DelegatedGroupChatAsync));
// Execute process
await RunProcessAsync(process);
}
[Fact]
public async Task SingleTeacherStudentAgentCallAsync()
{
// Define process
KernelProcess process = SetupSingleAgentProcess<BasicTeacherAgentInput>(nameof(SingleTeacherStudentAgentCallAsync));
// Setup kernel with OpenAI Client
Kernel kernel = SetupKernel();
// Execute process
await using LocalKernelProcessContext localProcess =
await process.StartAsync(
kernel,
new KernelProcessEvent()
{
Id = AgentOrchestrationEvents.StartProcess
});
// Cleaning up created agents
var processState = await localProcess.GetStateAsync();
var agentState = (KernelProcessStepState<KernelProcessAgentExecutorState>)processState.Steps.Where(step => step.State.Id == "Student").FirstOrDefault()!.State;
var agentId = agentState?.State?.AgentId;
if (agentId != null)
{
await this.Client.GetAssistantClient().DeleteAssistantAsync(agentId);
}
}
private sealed class BasicAgentChatUserInput : ScriptedUserInputStep
{
public BasicAgentChatUserInput()
{
this.SuppressOutput = true;
}
public override void PopulateUserInputs(UserInputState state)
{
state.UserInputs.Add("Hi");
state.UserInputs.Add("List the upcoming events on my calendar for the next week");
state.UserInputs.Add("Correct");
state.UserInputs.Add("When is an open time to go camping near home for 4 days after the end of this week?");
state.UserInputs.Add("Yes, and I'd prefer nice weather.");
state.UserInputs.Add("Sounds good, add the soonest option without conflicts to my calendar");
state.UserInputs.Add("Correct");
state.UserInputs.Add("That's all, thank you");
}
}
private sealed class BasicTeacherAgentInput : ScriptedUserInputStep
{
public BasicTeacherAgentInput()
{
this.SuppressOutput = true;
}
public override void PopulateUserInputs(UserInputState state)
{
state.UserInputs.Add("What is 2+2");
state.UserInputs.Add("What is 2+2");
state.UserInputs.Add("What is the name of a shape with 3 consecutive sides");
state.UserInputs.Add("What is the internal angle of a square");
state.UserInputs.Add("What is a parallellogram");
}
}
private async Task RunProcessAsync(KernelProcess process)
{
// Initialize services
ChatHistory history = [];
Kernel kernel = SetupKernel(history);
// Execute process
await using LocalKernelProcessContext localProcess =
await process.StartAsync(
kernel,
new KernelProcessEvent()
{
Id = AgentOrchestrationEvents.StartProcess
});
// Demonstrate history is maintained independent of process state
this.WriteHorizontalRule();
foreach (ChatMessageContent message in history)
{
RenderMessageStep.Render(message);
}
}
private KernelProcess SetupSingleAgentProcess<TUserInputStep>(string processName) where TUserInputStep : ScriptedUserInputStep
{
ProcessBuilder process = new(processName);
var userInputStep = process.AddStepFromType<TUserInputStep>();
var renderMessageStep = process.AddStepFromType<RenderMessageStep>();
var agentStep = process.AddStepFromAgent(new()
{
Name = "Student",
// On purpose not assigning AgentId, if not provided a new agent is created
Description = "Solves problem given",
Instructions = "Solve the problem given, if the question is repeated mention something like I already answered but here is the answer with a bit of humor",
Model = new()
{
Id = "gpt-4o",
},
Type = OpenAIAssistantAgentFactory.OpenAIAssistantAgentType,
});
// Entry point
process.OnInputEvent(AgentOrchestrationEvents.StartProcess)
.SendEventTo(new(userInputStep));
// Pass user input to primary agent
userInputStep
.OnEvent(CommonEvents.UserInputReceived)
.SendEventTo(new ProcessFunctionTargetBuilder(agentStep, parameterName: "message"))
.SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderUserText));
agentStep
.OnFunctionResult()
.SendEventTo(new ProcessFunctionTargetBuilder(userInputStep))
.SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderMessage));
agentStep
.OnFunctionError()
.SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderError, "error"))
.StopProcess();
return process.Build();
}
private KernelProcess SetupAgentProcess<TUserInputStep>(string processName) where TUserInputStep : ScriptedUserInputStep
{
ProcessBuilder process = new(processName);
var userInputStep = process.AddStepFromType<TUserInputStep>();
var renderMessageStep = process.AddStepFromType<RenderMessageStep>();
var managerAgentStep = process.AddStepFromType<ManagerAgentStep>();
var agentGroupStep = process.AddStepFromType<AgentGroupChatStep>();
AttachErrorStep(
userInputStep,
ScriptedUserInputStep.ProcessStepFunctions.GetUserInput);
AttachErrorStep(
managerAgentStep,
ManagerAgentStep.ProcessStepFunctions.InvokeAgent,
ManagerAgentStep.ProcessStepFunctions.InvokeGroup,
ManagerAgentStep.ProcessStepFunctions.ReceiveResponse);
AttachErrorStep(
agentGroupStep,
AgentGroupChatStep.ProcessStepFunctions.InvokeAgentGroup);
// Entry point
process.OnInputEvent(AgentOrchestrationEvents.StartProcess)
.SendEventTo(new ProcessFunctionTargetBuilder(userInputStep));
// Pass user input to primary agent
userInputStep
.OnEvent(CommonEvents.UserInputReceived)
.SendEventTo(new ProcessFunctionTargetBuilder(managerAgentStep, ManagerAgentStep.ProcessStepFunctions.InvokeAgent))
.SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderUserText, parameterName: "message"));
// Process completed
userInputStep
.OnEvent(CommonEvents.UserInputComplete)
.SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderDone))
.StopProcess();
// Render response from primary agent
managerAgentStep
.OnEvent(AgentOrchestrationEvents.AgentResponse)
.SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderMessage, parameterName: "message"));
// Request is complete
managerAgentStep
.OnEvent(CommonEvents.UserInputComplete)
.SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderDone))
.StopProcess();
// Request more user input
managerAgentStep
.OnEvent(AgentOrchestrationEvents.AgentResponded)
.SendEventTo(new ProcessFunctionTargetBuilder(userInputStep));
// Delegate to inner agents
managerAgentStep
.OnEvent(AgentOrchestrationEvents.AgentWorking)
.SendEventTo(new ProcessFunctionTargetBuilder(managerAgentStep, ManagerAgentStep.ProcessStepFunctions.InvokeGroup));
// Provide input to inner agents
managerAgentStep
.OnEvent(AgentOrchestrationEvents.GroupInput)
.SendEventTo(new ProcessFunctionTargetBuilder(agentGroupStep, parameterName: "input"));
// Render response from inner chat (for visibility)
agentGroupStep
.OnEvent(AgentOrchestrationEvents.GroupMessage)
.SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderInnerMessage, parameterName: "message"));
// Provide inner response to primary agent
agentGroupStep
.OnEvent(AgentOrchestrationEvents.GroupCompleted)
.SendEventTo(new ProcessFunctionTargetBuilder(managerAgentStep, ManagerAgentStep.ProcessStepFunctions.ReceiveResponse, parameterName: "response"));
KernelProcess kernelProcess = process.Build();
return kernelProcess;
void AttachErrorStep(ProcessStepBuilder step, params string[] functionNames)
{
foreach (string functionName in functionNames)
{
step
.OnFunctionError(functionName)
.SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderError, "error"))
.StopProcess();
}
}
}
private Kernel SetupKernel()
{
IKernelBuilder builder = Kernel.CreateBuilder();
// Add Chat Completion to Kernel
this.AddChatCompletionToKernel(builder);
builder.Services.AddSingleton<OpenAIClient>(this.Client);
// NOTE: Uncomment to see process logging
//builder.Services.AddSingleton<ILoggerFactory>(this.LoggerFactory);
return builder.Build();
}
private Kernel SetupKernel(ChatHistory history)
{
IKernelBuilder builder = Kernel.CreateBuilder();
// Add Chat Completion to Kernel
this.AddChatCompletionToKernel(builder);
// Inject agents into service collection
SetupAgents(builder, builder.Build());
// Inject history provider into service collection
builder.Services.AddSingleton<IChatHistoryProvider>(new ChatHistoryProvider(history));
// NOTE: Uncomment to see process logging
//builder.Services.AddSingleton<ILoggerFactory>(this.LoggerFactory);
return builder.Build();
}
private const string ManagerInstructions =
"""
Capture information provided by the user for their scheduling request.
Request confirmation without suggesting additional details.
Once confirmed inform them you're working on the request.
Never provide a direct answer to the user's request.
""";
private const string CalendarInstructions =
"""
Evaluate the scheduled calendar events in response to the current direction.
In the absence of specific dates, prioritize the earliest opportunity but do not restrict evaluation the current date.
Never consider or propose scheduling that conflicts with existing events.
""";
private const string WeatherInstructions =
"""
Provide weather information in response to the current direction.
""";
private const string ManagerSummaryInstructions =
"""
Summarize the most recent user request in first person command form.
""";
private const string SuggestionSummaryInstructions =
"""
Address the user directly with a summary of the response.
""";
private static void SetupAgents(IKernelBuilder builder, Kernel kernel)
{
// Create and inject primary agent into service collection
ChatCompletionAgent managerAgent = CreateAgent("Manager", ManagerInstructions, kernel.Clone());
builder.Services.AddKeyedSingleton(ManagerAgentStep.AgentServiceKey, managerAgent);
// Create and inject group chat into service collection
SetupGroupChat(builder, kernel);
// Create and inject reducers into service collection
builder.Services.AddKeyedSingleton(ManagerAgentStep.ReducerServiceKey, SetupReducer(kernel, ManagerSummaryInstructions));
builder.Services.AddKeyedSingleton(AgentGroupChatStep.ReducerServiceKey, SetupReducer(kernel, SuggestionSummaryInstructions));
}
private static ChatHistorySummarizationReducer SetupReducer(Kernel kernel, string instructions) =>
new(kernel.GetRequiredService<IChatCompletionService>(), 1)
{
SummarizationInstructions = instructions
};
private static void SetupGroupChat(IKernelBuilder builder, Kernel kernel)
{
const string CalendarAgentName = "CalendarAgent";
ChatCompletionAgent calendarAgent = CreateAgent(CalendarAgentName, CalendarInstructions, kernel.Clone());
calendarAgent.Kernel.Plugins.AddFromType<CalendarPlugin>();
const string WeatherAgentName = "WeatherAgent";
ChatCompletionAgent weatherAgent = CreateAgent(WeatherAgentName, WeatherInstructions, kernel.Clone());
weatherAgent.Kernel.Plugins.AddFromType<WeatherPlugin>();
weatherAgent.Kernel.Plugins.AddFromType<LocationPlugin>();
KernelFunction selectionFunction =
AgentGroupChat.CreatePromptFunctionForStrategy(
$$$"""
Determine which participant takes the next turn in a conversation based on the the most recent participant.
State only the name of the participant to take the next turn.
No participant should take more than one turn in a row.
Choose only from these participants:
- {{{CalendarAgentName}}}
- {{{WeatherAgentName}}}
Always follow these rules when selecting the next participant:
- After user input, it is {{{CalendarAgentName}}}'s turn.
- After {{{CalendarAgentName}}}, it is {{{WeatherAgentName}}}'s turn.
- After {{{WeatherAgentName}}}, it is {{{CalendarAgentName}}}'s turn.
History:
{{$history}}
""",
safeParameterNames: "history");
KernelFunction terminationFunction =
AgentGroupChat.CreatePromptFunctionForStrategy(
$$$"""
Evaluate if the a user's most recent calendar request has received a final response.
If weather conditions are requested, {{{WeatherAgentName}}} is required to provide input.
If all of these conditions are met, respond with a single word: yes
History:
{{$history}}
""",
safeParameterNames: "history");
AgentGroupChat chat =
new(calendarAgent, weatherAgent)
{
// NOTE: Replace logger when using outside of sample.
// Use `this.LoggerFactory` to observe logging output as part of sample.
LoggerFactory = NullLoggerFactory.Instance,
ExecutionSettings = new()
{
SelectionStrategy =
new KernelFunctionSelectionStrategy(selectionFunction, kernel)
{
HistoryVariableName = "history",
HistoryReducer = new ChatHistoryTruncationReducer(1),
ResultParser = (result) => result.GetValue<string>() ?? calendarAgent.Name!,
},
TerminationStrategy =
new KernelFunctionTerminationStrategy(terminationFunction, kernel)
{
HistoryVariableName = "history",
MaximumIterations = 12,
//HistoryReducer = new ChatHistoryTruncationReducer(2),
ResultParser = (result) => result.GetValue<string>()?.Contains("yes", StringComparison.OrdinalIgnoreCase) ?? false,
}
}
};
builder.Services.AddSingleton(chat);
}
private static ChatCompletionAgent CreateAgent(string name, string instructions, Kernel kernel) =>
new()
{
Name = name,
Instructions = instructions,
Kernel = kernel.Clone(),
Arguments =
new KernelArguments(
new OpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
Temperature = 0,
}),
};
}