I am working on an agent-based system using Microsoft's Semantic Kernel and am facing an issue with the event flow. My expected flow is:
Ask the user for a request
Confirm the request (yes → proceed, no → restart request)
Ask if they want an image (yes/no)
Process the request (generate response with or without an image) : i.e if user wants image then response will be Text + Image, else the response should be only text
I used this repo, modified for my workflow
Current Implementation
I have set up an event-driven process using KernelProcess and ProcessStepBuilder. My process follows these steps:
- The WelcomeStep asks for user input.
- The UserInputStep gets user confirmation.
- If the user confirms, it asks whether they want an image.
- Based on the response, the request is processed.
The issue:
Even user does not want an image, the image is generated when completing the process. Because I don't know how to handle this. I need to generate image conditionally or I need another solution rather than handling conditional in events in program.cs. Please suggest a solution
Program.cs
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.Chat;
using Microsoft.SemanticKernel.Agents.History;
using Microsoft.SemanticKernel.ChatCompletion;
var process = SetupAgentProcess(nameof(Demo));
// Execute process
using var localProcess = await process.StartAsync(kernel, new KernelProcessEvent()
{
Id = Events.StartProcess
});
static KernelProcess SetupAgentProcess(string processName)
{
ProcessBuilder process = new(processName);
var welcomeStep = process.AddStepFromType<WelcomeStep>();
var userInputStep = process.AddStepFromType<UserInputStep>();
var renderMessageStep = process.AddStepFromType<RenderMessageStep>();
var managerAgentStep = process.AddStepFromType<ManagerAgentStep>();
var agentGroupStep = process.AddStepFromType<AgentGroupChatStep>();
var imageCreatorStep = process.AddStepFromType<ImageCreatorStep>();
AttachErrorStep(
userInputStep,
UserInputStep.Functions.GetUserInput);
AttachErrorStep(
managerAgentStep,
ManagerAgentStep.Functions.InvokeAgent,
ManagerAgentStep.Functions.InvokeGroup,
ManagerAgentStep.Functions.ReceiveResponse);
AttachErrorStep(
agentGroupStep,
AgentGroupChatStep.Functions.InvokeAgentGroup);
// Entry point
process.OnInputEvent(Events.StartProcess)
.SendEventTo(new ProcessFunctionTargetBuilder(welcomeStep));
// Once the welcome message has been shown, request user input
welcomeStep.OnFunctionResult(WelcomeStep.Functions.WelcomeMessage)
.SendEventTo(new ProcessFunctionTargetBuilder(userInputStep, UserInputStep.Functions.GetUserInput));
// Pass user input to primary agent
userInputStep
.OnEvent(Events.UserInputReceived)
.SendEventTo(new ProcessFunctionTargetBuilder(managerAgentStep, ManagerAgentStep.Functions.InvokeAgent));
// Render response from primary agent
managerAgentStep
.OnEvent(Events.Agents.AgentResponse)
.SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.Functions.RenderMessage, parameterName: "message"));
// Request is complete
managerAgentStep
.OnEvent(Events.UserInputComplete)
.SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.Functions.RenderDone))
.StopProcess();
managerAgentStep
.OnEvent(Events.Agents.AgentRequestUserConfirmation)
.SendEventTo(new ProcessFunctionTargetBuilder(userInputStep, UserInputStep.Functions.GetUserConfirmation));
// Once the user confirms, ask if they want an image
userInputStep
.OnEvent(Events.UserConfirmedRequest)
.SendEventTo(new ProcessFunctionTargetBuilder(userInputStep, UserInputStep.Functions.GetImagePreference));
// trigger invokeagent once UserImagePreferenceReceived
userInputStep
.OnEvent(Events.UserImagePreferenceReceived)
.SendEventTo(new ProcessFunctionTargetBuilder(managerAgentStep, ManagerAgentStep.Functions.InvokeAgent));
// Request more user input
managerAgentStep
.OnEvent(Events.Agents.AgentResponded) // once agent response with image is generated event status goes to AgentResponded.
.SendEventTo(new ProcessFunctionTargetBuilder(userInputStep, UserInputStep.Functions.GetUserInput));// so this fucntion triggerd
// Delegate to inner agents
managerAgentStep
.OnEvent(Events.Agents.AgentWorking) // after confirmed event goes to AgentWorking and InvokeGroup trirgger
.SendEventTo(new ProcessFunctionTargetBuilder(managerAgentStep, ManagerAgentStep.Functions.InvokeGroup));
// Provide input to inner agents
managerAgentStep
.OnEvent(Events.Agents.GroupInput)
.SendEventTo(new ProcessFunctionTargetBuilder(agentGroupStep, parameterName: "input"));
// Step 1: Render response from inner chat for visibility
agentGroupStep
.OnEvent(Events.Agents.GroupMessage)
.SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.Functions.RenderInnerMessage, parameterName: "message"));
// Provide inner response to primary agent
agentGroupStep
.OnEvent(Events.Agents.GroupCompleted)
.SendEventTo(new ProcessFunctionTargetBuilder(imageCreatorStep, ImageCreatorStep.Functions.CreateImageForCopy, parameterName: "copy")); // I need to handle the conditionally
// Step 3: Once the image is created, proceed to the managerAgentStep
imageCreatorStep
.OnEvent(Events.ImageCreated)
.SendEventTo(new ProcessFunctionTargetBuilder(managerAgentStep, ManagerAgentStep.Functions.ReceiveResponse, parameterName: "response"));
var kernelProcess = process.Build();
return kernelProcess;
void AttachErrorStep(ProcessStepBuilder step, params string[] functionNames)
{
foreach (var functionName in functionNames)
{
step.OnFunctionError(functionName)
.SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.Functions.RenderError, "error"))
.StopProcess();
}
}
}
UserInputStep.cs
internal sealed class UserInputStep : KernelProcessStep<UserInputState>
{
private UserInputState state;
public static class Functions
{
public const string GetUserInput = nameof(GetUserInput);
public const string GetUserConfirmation = nameof(GetUserConfirmation);
public const string GetImagePreference = nameof(GetImagePreference);
}
public override ValueTask ActivateAsync(KernelProcessStepState<UserInputState> state)
{
this.state = state.State!;
return ValueTask.CompletedTask;
}
[KernelFunction(Functions.GetUserInput)]
public async ValueTask GetUserInputAsync(KernelProcessStepContext context)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("\nPlease enter a marketing request, or say «bye» to exit: ");
Console.ForegroundColor = ConsoleColor.White;
var userMessage = Console.ReadLine();
if (userMessage?.StartsWith(@"bye", StringComparison.OrdinalIgnoreCase) ?? false)
{
await context.EmitEventAsync(new() { Id = Events.Exit, Data = userMessage });
return;
}
state.UserInputs.Add(userMessage!);
state.CurrentInputIndex++;
await context.EmitEventAsync(new() { Id = Events.UserInputReceived, Data = userMessage });
}
[KernelFunction(Functions.GetUserConfirmation)]
public async ValueTask GetUserConfirmationAsync(KernelProcessStepContext context)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("\nPlease confirm your request (yes/no), or provide a new marketing request. Just say «bye» to exit: ");
Console.ForegroundColor = ConsoleColor.White;
var userMessage = Console.ReadLine()?.Trim().ToLower();
if (userMessage == "bye")
{
await context.EmitEventAsync(new() { Id = Events.Exit, Data = userMessage });
return;
}
if (userMessage == "yes")
{
await context.EmitEventAsync(new() { Id = Events.UserConfirmedRequest });
}
else
{
state.UserInputs.Add(userMessage!);
state.CurrentInputIndex++;
await context.EmitEventAsync(new() { Id = Events.UserInputReceived, Data = userMessage });
}
}
[KernelFunction(Functions.GetImagePreference)]
public async ValueTask GetImagePreferenceAsync(KernelProcessStepContext context)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("\nDo you want an image with your response? (yes/no): ");
Console.ForegroundColor = ConsoleColor.White;
var userResponse = Console.ReadLine()?.Trim().ToLower();
if (userResponse == "yes")
{
// If user says yes, set the WantsImage flag to true
state.WantsImage = true;
await context.EmitEventAsync(new() { Id = Events.UserImagePreferenceReceived, Data = true });
}
else if (userResponse == "no")
{
// If user says no, set the WantsImage flag to false
state.WantsImage = false;
await context.EmitEventAsync(new() { Id = Events.UserImagePreferenceReceived, Data = false });
}
else
{
// If the response is invalid (neither yes nor no), ask again
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Invalid input! Please respond with 'yes' or 'no'.");
Console.ForegroundColor = ConsoleColor.White;
// Re-prompt for image preference
await GetImagePreferenceAsync(context);
}
}
}
public record UserInputState
{
public List<string> UserInputs { get; init; } = [];
public int CurrentInputIndex { get; set; } = 0;
public bool WantsImage { get; set; } = false; // I could not this state value in program.cs to hanlde the image generation part conditionally
}
Am I missing something in event handling within ProcessFunctionTargetBuilder or KernelProcess?
Update:
Can I read the state of UserInputStep in ImageCreatorStep? How ?
If possible I can handle conditionally (wants image/not) inside ImageCreatorStep.
internal sealed class ImageCreatorStep : KernelProcessStep
{
public static class Functions
{
// public static bool RequiresImage;
public const string CreateImageForCopy = nameof(CreateImageForCopy);
}
[KernelFunction(Functions.CreateImageForCopy)]
public static async Task CreateImageForCopyAsync(KernelProcessStepContext context, Kernel kernel, string copy)
{
var userState = kernel.GetRequiredService<KernelProcessStepState<UserInputState>>()?.State; // This also did not work, I get empty value for each property in userState
var imageTask = await kernel.GetRequiredService<ITextToImageService>().GenerateImageAsync(copy, 1024, 1024);
var result = $"Copy: {copy}\n\nImage URL: {imageTask}";
await context.EmitEventAsync(new() { Id = Events.ImageCreated, Data = result });
}
}