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