// Copyright (c) Microsoft. All rights reserved.
using Events;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Process.Tools;
using SharedSteps;
using Utilities;
namespace Step01;
///
/// Demonstrate creation of and
/// eliciting its response to three explicit user messages.
///
public class Step01_Processes(ITestOutputHelper output) : BaseTest(output, redirectSystemConsoleOutput: true)
{
///
/// Demonstrates the creation of a simple process that has multiple steps, takes
/// user input, interacts with the chat completion service, and demonstrates cycles
/// in the process.
///
/// A
[Fact]
public async Task UseSimpleProcessAsync()
{
// Create a kernel with a chat completion service
Kernel kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion(
modelId: TestConfiguration.OpenAI.ChatModelId,
apiKey: TestConfiguration.OpenAI.ApiKey)
.Build();
// Create a process that will interact with the chat completion service
ProcessBuilder process = new("ChatBot");
var introStep = process.AddStepFromType();
var userInputStep = process.AddStepFromType();
var responseStep = process.AddStepFromType();
// Define the behavior when the process receives an external event
process
.OnInputEvent(ChatBotEvents.StartProcess)
.SendEventTo(new ProcessFunctionTargetBuilder(introStep));
// When the intro is complete, notify the userInput step
introStep
.OnFunctionResult()
.SendEventTo(new ProcessFunctionTargetBuilder(userInputStep));
// When the userInput step emits an exit event, send it to the end step
userInputStep
.OnEvent(ChatBotEvents.Exit)
.StopProcess();
// When the userInput step emits a user input event, send it to the assistantResponse step
userInputStep
.OnEvent(CommonEvents.UserInputReceived)
.SendEventTo(new ProcessFunctionTargetBuilder(responseStep, parameterName: "userMessage"));
// When the assistantResponse step emits a response, send it to the userInput step
responseStep
.OnEvent(ChatBotEvents.AssistantResponseGenerated)
.SendEventTo(new ProcessFunctionTargetBuilder(userInputStep));
// Build the process to get a handle that can be started
KernelProcess kernelProcess = process.Build();
// Generate a Mermaid diagram for the process and print it to the console
string mermaidGraph = kernelProcess.ToMermaid();
Console.WriteLine($"=== Start - Mermaid Diagram for '{process.Name}' ===");
Console.WriteLine(mermaidGraph);
Console.WriteLine($"=== End - Mermaid Diagram for '{process.Name}' ===");
// Generate an image from the Mermaid diagram
string generatedImagePath = await MermaidRenderer.GenerateMermaidImageAsync(mermaidGraph, "ChatBotProcess.png");
Console.WriteLine($"Diagram generated at: {generatedImagePath}");
// Start the process with an initial external event
await using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent() { Id = ChatBotEvents.StartProcess, Data = null });
}
///
/// The simplest implementation of a process step. IntroStep
///
private sealed class IntroStep : KernelProcessStep
{
///
/// Prints an introduction message to the console.
///
[KernelFunction]
public void PrintIntroMessage()
{
System.Console.WriteLine("Welcome to Processes in Semantic Kernel.\n");
}
}
///
/// A step that elicits user input.
///
private sealed class ChatUserInputStep : ScriptedUserInputStep
{
public override void PopulateUserInputs(UserInputState state)
{
state.UserInputs.Add("Hello");
state.UserInputs.Add("How tall is the tallest mountain?");
state.UserInputs.Add("How low is the lowest valley?");
state.UserInputs.Add("How wide is the widest river?");
state.UserInputs.Add("exit");
state.UserInputs.Add("This text will be ignored because exit process condition was already met at this point.");
}
public override async ValueTask GetUserInputAsync(KernelProcessStepContext context)
{
var userMessage = this.GetNextUserMessage();
if (string.Equals(userMessage, "exit", StringComparison.OrdinalIgnoreCase))
{
// exit condition met, emitting exit event
await context.EmitEventAsync(new() { Id = ChatBotEvents.Exit, Data = userMessage });
return;
}
// emitting userInputReceived event
await context.EmitEventAsync(new() { Id = CommonEvents.UserInputReceived, Data = userMessage });
}
}
///
/// A step that takes the user input from a previous step and generates a response from the chat completion service.
///
private sealed class ChatBotResponseStep : KernelProcessStep
{
public static class ProcessFunctions
{
public const string GetChatResponse = nameof(GetChatResponse);
}
///
/// The internal state object for the chat bot response step.
///
internal ChatBotState? _state;
///
/// ActivateAsync is the place to initialize the state object for the step.
///
/// An instance of
/// A
public override ValueTask ActivateAsync(KernelProcessStepState state)
{
_state = state.State;
return ValueTask.CompletedTask;
}
///
/// Generates a response from the chat completion service.
///
/// The context for the current step and process.
/// The user message from a previous step.
/// A instance.
///
[KernelFunction(ProcessFunctions.GetChatResponse)]
public async Task GetChatResponseAsync(KernelProcessStepContext context, string userMessage, Kernel _kernel)
{
_state!.ChatMessages.Add(new(AuthorRole.User, userMessage));
IChatCompletionService chatService = _kernel.Services.GetRequiredService();
ChatMessageContent response = await chatService.GetChatMessageContentAsync(_state.ChatMessages).ConfigureAwait(false);
if (response == null)
{
throw new InvalidOperationException("Failed to get a response from the chat completion service.");
}
System.Console.ForegroundColor = ConsoleColor.Yellow;
System.Console.WriteLine($"ASSISTANT: {response.Content}");
System.Console.ResetColor();
// Update state with the response
_state.ChatMessages.Add(response);
// emit event: assistantResponse
await context.EmitEventAsync(new KernelProcessEvent { Id = ChatBotEvents.AssistantResponseGenerated, Data = response });
}
}
///
/// The state object for the .
///
private sealed class ChatBotState
{
internal ChatHistory ChatMessages { get; } = [];
}
///
/// A class that defines the events that can be emitted by the chat bot process. This is
/// not required but used to ensure that the event names are consistent.
///
private static class ChatBotEvents
{
public const string StartProcess = "startProcess";
public const string IntroComplete = "introComplete";
public const string AssistantResponseGenerated = "assistantResponseGenerated";
public const string Exit = "exit";
}
}