chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,446 @@
|
||||
// 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,
|
||||
}),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user