chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Step04;
/// <summary>
/// Processes events used in <see cref="Step04_AgentOrchestration"/> samples
/// </summary>
public static class AgentOrchestrationEvents
{
public static readonly string StartProcess = nameof(StartProcess);
public static readonly string AgentResponse = nameof(AgentResponse);
public static readonly string AgentResponded = nameof(AgentResponded);
public static readonly string AgentWorking = nameof(AgentWorking);
public static readonly string GroupInput = nameof(GroupInput);
public static readonly string GroupMessage = nameof(GroupMessage);
public static readonly string GroupCompleted = nameof(GroupCompleted);
}
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel.ChatCompletion;
namespace Step04;
/// <summary>
/// Provider based access to the chat history.
/// </summary>
/// <remarks>
/// While the in-memory implementation is trivial, this abstraction demonstrates how one might
/// allow for the ability to access chat history from a remote store for a distributed service.
/// <code>
/// class CosmosDbChatHistoryProvider(CosmosClient client, string sessionId) : IChatHistoryProvider { }
/// </code>
/// </remarks>
internal interface IChatHistoryProvider
{
/// <summary>
/// Provides access to the chat history.
/// </summary>
Task<ChatHistory> GetHistoryAsync();
/// <summary>
/// Commits any updates to the chat history.
/// </summary>
Task CommitAsync();
}
/// <summary>
/// In memory based specialization of <see cref="IChatHistoryProvider"/>.
/// </summary>
internal sealed class ChatHistoryProvider(ChatHistory history) : IChatHistoryProvider
{
/// <inheritdoc/>
public Task<ChatHistory> GetHistoryAsync() => Task.FromResult(history);
/// <inheritdoc/>
public Task CommitAsync()
{
return Task.CompletedTask;
}
}
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
namespace Step04;
/// <summary>
/// Convenience extensions for agent based process patterns.
/// </summary>
internal static class KernelExtensions
{
/// <summary>
/// Return chat history from a singleton <see cref="IChatHistoryProvider"/>.
/// </summary>
public static IChatHistoryProvider GetHistory(this Kernel kernel) =>
kernel.Services.GetRequiredService<IChatHistoryProvider>();
/// <summary>
/// Access an agent as a keyed service.
/// </summary>
public static TAgent GetAgent<TAgent>(this Kernel kernel, string key) where TAgent : Agent =>
kernel.Services.GetRequiredKeyedService<TAgent>(key);
/// <summary>
/// Summarize chat history using reducer accessed as a keyed service.
/// </summary>
public static async Task<string> SummarizeHistoryAsync(this Kernel kernel, string key, IReadOnlyList<ChatMessageContent> history)
{
ChatHistorySummarizationReducer reducer = kernel.Services.GetRequiredKeyedService<ChatHistorySummarizationReducer>(key);
IEnumerable<ChatMessageContent>? reducedResponse = await reducer.ReduceAsync(history);
ChatMessageContent summary = reducedResponse?.First() ?? throw new InvalidDataException("No summary available");
return summary.ToString();
}
}
@@ -0,0 +1,152 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using System.Globalization;
using System.Text.Json.Serialization;
using Microsoft.SemanticKernel;
namespace Step04.Plugins;
internal sealed record CalendarEvent(
string Title,
string Start,
string? End,
string? Description = null)
{
[JsonIgnore]
public DateTime StartDate { get; } = DateTime.Parse(Start);
[JsonIgnore]
public DateTime? EndDate { get; } = End != null ? DateTime.Parse(End) : null;
}
/// <summary>
/// Mock plug-in to provide calendar information for the current and following month.
/// </summary>
/// <remarks>
/// Calendar information is simplified in the sense that any event covers the entire
/// day. Also, no special treatment for weekend.
/// </remarks>
internal sealed class CalendarPlugin
{
private static readonly DateTime s_now = DateTime.Now;
private static readonly DateTime s_nextMonth = new(s_now.Year, DateTime.Now.AddMonths(1).Month, 1);
private readonly List<CalendarEvent> _events = [];
public CalendarPlugin()
{
CalendarGenerator generator = new();
this._events =
[
.. generator.GenerateEvents(s_now.Month, s_now.Year),
.. generator.GenerateEvents(s_nextMonth.Month, s_nextMonth.Year),
];
}
// Exposed for validation / no impact to plugin functionality
public IReadOnlyList<CalendarEvent> Events => this._events;
[KernelFunction]
public string GetCurrentDate() => DateTime.Now.Date.ToString("dd-MMM-yyyy");
[KernelFunction]
[Description("Get the scheduled events that begin within the specified date range.")]
public IReadOnlyList<CalendarEvent> GetEvents(
[Description("The first date in the range")]
string start,
[Description("The final date in the range")]
string end)
{
DateTime startDate = DateTime.Parse(start, CultureInfo.CurrentCulture);
DateTime endDate = DateTime.Parse(end, CultureInfo.CurrentCulture);
return this._events.Where(e => e.StartDate.Date >= startDate.Date && e.StartDate.Date < endDate.Date.AddDays(1)).ToArray();
}
[KernelFunction]
[Description("Create a new scheduled event.")]
public void NewEvent(string title, string startDate, string? endDate = null, string? description = null)
{
_events.Add(new CalendarEvent(title, startDate, endDate, description));
}
private sealed class CalendarGenerator
{
public int MaximumMultiDayEventCount => this._multiDayEvents.Count;
public int MinimumEventGapInDays => 3; // Personal calendar less dense
public IEnumerable<CalendarEvent> GenerateEvents(int month, int year)
{
int targetDayOfMonth = 1;
do
{
bool isMultiDay = Random.Shared.Next(5) == 0 && this.MaximumMultiDayEventCount > 0;
int daySpan = Random.Shared.Next(2, 8);
int gapDays = Random.Shared.Next(this.MinimumEventGapInDays, 5);
(string title, string description) = this.Pick(!isMultiDay);
yield return new CalendarEvent(
title,
FormatDate(targetDayOfMonth),
isMultiDay ? FormatDate(targetDayOfMonth, daySpan) : null,
description);
targetDayOfMonth += gapDays + 1 + (isMultiDay ? daySpan : 0);
}
while (targetDayOfMonth <= DateTime.DaysInMonth(year, month));
string FormatDate(int day, int span = 0)
{
DateOnly date = new(year, month, day);
date = date.AddDays(span);
return date.ToString("dd-MMM-yyyy");
}
}
private (string title, string description) Pick(bool isSingleDay)
{
return Pick(isSingleDay ? _singleDayEvents : _multiDayEvents);
}
private static (string title, string description) Pick(List<(string title, string description)> eventList)
{
int index = Random.Shared.Next(eventList.Count);
try
{
return eventList[index];
}
finally
{
eventList.RemoveAt(index);
}
}
public readonly List<(string title, string description)> _singleDayEvents =
[
("Doctor's Appointment", "Annual physical check-up."),
("Grocery Shopping", "Weekly stock-up on essentials."),
("Yoga Class", "1-hour morning yoga session."),
("Car Maintenance", "Oil change and tire rotation."),
("Dinner with Friends", "Casual dinner at local restaurant."),
("Team Meeting", "Project update and discussion with the team."),
("Haircut Appointment", "Haircut and style at the salon."),
("Parent-Teacher Conference", "Discuss child's progress in school."),
("Dentist Appointment", "Teeth cleaning and routine check-up."),
("Workout Session", "Strength training at the gym."),
("Birthday Party", "Attending a friend's birthday celebration."),
("Movie Night", "Watch new release at the theater."),
("Volunteer Work", "Community cleanup event participation."),
("Job Interview", "Interview for potential new role."),
("Library Visit", "Return books and browse for new reads.")
];
public readonly List<(string title, string description)> _multiDayEvents =
[
("Vacation", "Relaxing trip with family."),
("Home Renovation Project", "Kitchen remodeling."),
("Annual Family Reunion", "Traveling to grandparents."),
];
}
}
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using Microsoft.SemanticKernel;
namespace Step04.Plugins;
/// <summary>
/// Mock plug-in to provide location information.
/// </summary>
internal sealed class LocationPlugin
{
[KernelFunction]
[Description("Provide the user's current location by city, region, and country.")]
public string GetCurrentLocation() => "Bellevue, WA, USA";
[KernelFunction]
[Description("Provide the user's home location by city, region, and country.")]
public string GetHomeLocation() => "Seattle, WA, USA";
[KernelFunction]
[Description("Provide the user's work office location by city, region, and country.")]
public string GetOfficeLocation() => "Redmond, WA, USA";
}
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using Microsoft.SemanticKernel;
namespace Step04.Plugins;
internal sealed record WeatherForecast(
string Date,
string Location,
string HighTemperature,
string LowTemperature,
string Precipition);
/// <summary>
/// Mock plug-in to provide weather information.
/// </summary>
internal sealed class WeatherPlugin
{
private readonly Dictionary<string, WeatherForecast> _forecasts = [];
[KernelFunction]
public string GetCurrentDate() => DateTime.Now.Date.ToString("dd-MMM-yyyy");
[KernelFunction]
[Description("Provide the weather forecast for the given date and location. Dates farther than 15 days out will use historical data.")]
public WeatherForecast GetForecast(
string date,
string location)
{
string key = $"{date}-{location}";
if (!this._forecasts.TryGetValue(key, out WeatherForecast? forecast))
{
forecast = GenerateForecast(date, location);
this._forecasts[key] = forecast;
}
return forecast;
}
private static WeatherForecast GenerateForecast(string date, string location)
{
int highTemp = Random.Shared.Next(49, 96);
int lowTemp = highTemp - Random.Shared.Next(12, 20);
int precip = Random.Shared.Next(0, 80);
return
new WeatherForecast(
date,
location,
$"{highTemp} F",
$"{lowTemp} F",
$"{precip} %");
}
}
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
using Microsoft.SemanticKernel;
namespace Step04;
internal static class JsonSchemaGenerator
{
private static readonly AIJsonSchemaCreateOptions s_config = new()
{
TransformOptions = new()
{
DisallowAdditionalProperties = true,
RequireAllProperties = true,
MoveDefaultKeywordToDescription = true,
}
};
/// <summary>
/// Wrapper for generating a JSON schema as string from a .NET type.
/// </summary>
public static string FromType<TSchemaType>()
{
return KernelJsonSchemaBuilder.Build(typeof(TSchemaType), "Intent Result", s_config).AsJson();
}
}
@@ -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,
}),
};
}
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
namespace Step04.Steps;
/// <summary>
/// This steps defines actions for the group chat in which to agents collaborate in
/// response to input from the primary agent.
/// </summary>
public class AgentGroupChatStep : KernelProcessStep
{
public const string ChatServiceKey = $"{nameof(AgentGroupChatStep)}:{nameof(ChatServiceKey)}";
public const string ReducerServiceKey = $"{nameof(AgentGroupChatStep)}:{nameof(ReducerServiceKey)}";
public static class ProcessStepFunctions
{
public const string InvokeAgentGroup = nameof(InvokeAgentGroup);
}
[KernelFunction(ProcessStepFunctions.InvokeAgentGroup)]
public async Task InvokeAgentGroupAsync(KernelProcessStepContext context, Kernel kernel, string input)
{
AgentGroupChat chat = kernel.GetRequiredService<AgentGroupChat>();
// Reset chat state from previous invocation
//await chat.ResetAsync();
chat.IsComplete = false;
ChatMessageContent message = new(AuthorRole.User, input);
chat.AddChatMessage(message);
await context.EmitEventAsync(new() { Id = AgentOrchestrationEvents.GroupMessage, Data = message });
await foreach (ChatMessageContent response in chat.InvokeAsync())
{
await context.EmitEventAsync(new() { Id = AgentOrchestrationEvents.GroupMessage, Data = response });
}
ChatMessageContent[] history = await chat.GetChatMessagesAsync().Reverse().ToArrayAsync();
// Summarize the group chat as a response to the primary agent
string summary = await kernel.SummarizeHistoryAsync(ReducerServiceKey, history);
await context.EmitEventAsync(new() { Id = AgentOrchestrationEvents.GroupCompleted, Data = summary });
}
}
@@ -0,0 +1,120 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using System.Text.Json;
using Events;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
namespace Step04.Steps;
/// <summary>
/// This steps defines actions for the primary agent. This agent is responsible forinteracting with
/// the user as well as as delegating to a group of agents.
/// </summary>
public class ManagerAgentStep : KernelProcessStep
{
public const string AgentServiceKey = $"{nameof(ManagerAgentStep)}:{nameof(AgentServiceKey)}";
public const string ReducerServiceKey = $"{nameof(ManagerAgentStep)}:{nameof(ReducerServiceKey)}";
public static class ProcessStepFunctions
{
public const string InvokeAgent = nameof(InvokeAgent);
public const string InvokeGroup = nameof(InvokeGroup);
public const string ReceiveResponse = nameof(ReceiveResponse);
}
[KernelFunction(ProcessStepFunctions.InvokeAgent)]
public async Task InvokeAgentAsync(KernelProcessStepContext context, Kernel kernel, string userInput, ILogger logger)
{
// Get the chat history
IChatHistoryProvider historyProvider = kernel.GetHistory();
ChatHistory history = await historyProvider.GetHistoryAsync();
ChatHistoryAgentThread agentThread = new(history);
// Obtain the agent response
ChatCompletionAgent agent = kernel.GetAgent<ChatCompletionAgent>(AgentServiceKey);
await foreach (ChatMessageContent message in agent.InvokeAsync(new ChatMessageContent(AuthorRole.User, userInput), agentThread))
{
// Both the input message and response message will automatically be added to the thread, which will update the internal chat history.
// Emit event for each agent response
await context.EmitEventAsync(new() { Id = AgentOrchestrationEvents.AgentResponse, Data = message });
}
// Commit any changes to the chat history
await historyProvider.CommitAsync();
// Evaluate current intent
IntentResult intent = await IsRequestingUserInputAsync(kernel, history, logger);
string intentEventId =
intent.IsRequestingUserInput ?
AgentOrchestrationEvents.AgentResponded :
intent.IsWorking ?
AgentOrchestrationEvents.AgentWorking :
CommonEvents.UserInputComplete;
await context.EmitEventAsync(new() { Id = intentEventId });
}
[KernelFunction(ProcessStepFunctions.InvokeGroup)]
public async Task InvokeGroupAsync(KernelProcessStepContext context, Kernel kernel)
{
// Get the chat history
IChatHistoryProvider historyProvider = kernel.GetHistory();
ChatHistory history = await historyProvider.GetHistoryAsync();
// Summarize the conversation with the user to use as input to the agent group
string summary = await kernel.SummarizeHistoryAsync(ReducerServiceKey, history);
await context.EmitEventAsync(new() { Id = AgentOrchestrationEvents.GroupInput, Data = summary });
}
[KernelFunction(ProcessStepFunctions.ReceiveResponse)]
public async Task ReceiveResponseAsync(KernelProcessStepContext context, Kernel kernel, string response)
{
// Get the chat history
IChatHistoryProvider historyProvider = kernel.GetHistory();
ChatHistory history = await historyProvider.GetHistoryAsync();
// Proxy the inner response
ChatCompletionAgent agent = kernel.GetAgent<ChatCompletionAgent>(AgentServiceKey);
ChatMessageContent message = new(AuthorRole.Assistant, response) { AuthorName = agent.Name };
history.Add(message);
await context.EmitEventAsync(new() { Id = AgentOrchestrationEvents.AgentResponse, Data = message });
await context.EmitEventAsync(new() { Id = AgentOrchestrationEvents.AgentResponded });
}
private static async Task<IntentResult> IsRequestingUserInputAsync(Kernel kernel, ChatHistory history, ILogger logger)
{
ChatHistory localHistory =
[
new ChatMessageContent(AuthorRole.System, "Analyze the conversation and determine if user input is being solicited."),
.. history.TakeLast(1)
];
IChatCompletionService service = kernel.GetRequiredService<IChatCompletionService>();
ChatMessageContent response = await service.GetChatMessageContentAsync(localHistory, new OpenAIPromptExecutionSettings { ResponseFormat = typeof(IntentResult) });
IntentResult intent = JsonSerializer.Deserialize<IntentResult>(response.ToString())!;
logger.LogTrace("{StepName} Response Intent - {IsRequestingUserInput}: {Rationale}", nameof(ManagerAgentStep), intent.IsRequestingUserInput, intent.Rationale);
return intent;
}
[DisplayName("IntentResult")]
[Description("this is the result description")]
public sealed record IntentResult(
[property:Description("True if user input is requested or solicited. Addressing the user with no specific request is False. Asking a question to the user is True.")]
bool IsRequestingUserInput,
[property:Description("True if the user request is being worked on.")]
bool IsWorking,
[property:Description("Rationale for the value assigned to IsRequestingUserInput")]
string Rationale);
}
@@ -0,0 +1,94 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
namespace Step04.Steps;
/// <summary>
/// Displays output to the user. While in this case it is just writing to the console,
/// in a real-world scenario this would be a more sophisticated rendering system. Isolating this
/// rendering logic from the internal logic of other process steps simplifies responsibility contract
/// and simplifies testing and state management.
/// </summary>
public class RenderMessageStep : KernelProcessStep
{
public static class ProcessStepFunctions
{
public const string RenderDone = nameof(RenderMessageStep.RenderDone);
public const string RenderError = nameof(RenderMessageStep.RenderError);
public const string RenderInnerMessage = nameof(RenderMessageStep.RenderInnerMessage);
public const string RenderMessage = nameof(RenderMessageStep.RenderMessage);
public const string RenderUserText = nameof(RenderMessageStep.RenderUserText);
}
private static readonly Stopwatch s_timer = Stopwatch.StartNew();
/// <summary>
/// Render an explicit message to indicate the process has completed in the expected state.
/// </summary>
/// <remarks>
/// If this message isn't rendered, the process is considered to have failed.
/// </remarks>
[KernelFunction]
public void RenderDone()
{
Render("DONE!");
}
/// <summary>
/// Render exception
/// </summary>
[KernelFunction]
public void RenderError(KernelProcessError error, ILogger logger)
{
string message = string.IsNullOrWhiteSpace(error.Message) ? "Unexpected failure" : error.Message;
Render($"ERROR: {message} [{error.GetType().Name}]{Environment.NewLine}{error.StackTrace}");
logger.LogError("Unexpected failure: {ErrorMessage} [{ErrorType}]", error.Message, error.Type);
}
/// <summary>
/// Render user input
/// </summary>
[KernelFunction]
public void RenderUserText(string message)
{
Render($"{AuthorRole.User.Label.ToUpperInvariant()}: {message}");
}
/// <summary>
/// Render an assistant message from the primary chat
/// </summary>
[KernelFunction]
public void RenderMessage(ChatMessageContent? message)
{
if (message is null)
{
// if the message is empty, we don't want to render it
return;
}
Render(message);
}
/// <summary>
/// Render an assistant message from the inner chat
/// </summary>
[KernelFunction]
public void RenderInnerMessage(ChatMessageContent message)
{
Render(message, indent: true);
}
public static void Render(ChatMessageContent message, bool indent = false)
{
string displayName = !string.IsNullOrWhiteSpace(message.AuthorName) ? $" - {message.AuthorName}" : string.Empty;
Render($"{(indent ? "\t" : string.Empty)}{message.Role.Label.ToUpperInvariant()}{displayName}: {message.Content}");
}
public static void Render(string message)
{
Console.WriteLine($"[{s_timer.Elapsed:mm\\:ss}] {message}");
}
}