From 4af7dfcb4578222ab49902f48c803151581f9fa5 Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Fri, 26 Apr 2024 10:08:29 -0700 Subject: [PATCH] .Net - Agents KernelFunction Strategies (#5895) ### Motivation and Context Users of `AgentGroupChat` will likely require strategies that rely on AI processing. Text processing alone is insufficient, but developers need other options than calling an AI model. With this update, our strategy story supports either. ### Description Introducing support `SelectionStrategy` and `TerminationStrategy` that utilize LLM processing with support for result processing. - `KernelFunctionSelectionStrategy` - `KernelFunctionTerminationStrategy` ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone :smile: --- .../Concepts/AgentSyntax/AgentSyntax.csproj | 6 +- .../samples/Concepts/AgentSyntax/BaseTest.cs | 2 +- .../AgentSyntax/Getting_Started/Step3_Chat.cs | 2 +- .../Step4_KernelFunctionStrategies.cs | 127 ++++++++++++++++++ .../Getting_Started/Step5_JsonResult.cs | 92 +++++++++++++ .../RepoUtils/JsonResultTranslator.cs | 79 +++++++++++ dotnet/src/Agents/Abstractions/KernelAgent.cs | 2 +- dotnet/src/Agents/Core/Agents.Core.csproj | 1 + .../Chat/KernelFunctionSelectionStrategy.cs | 84 ++++++++++++ .../Chat/KernelFunctionTerminationStrategy.cs | 76 +++++++++++ .../KernelFunctionSelectionStrategyTests.cs | 67 +++++++++ .../KernelFunctionTerminationStrategyTests.cs | 71 ++++++++++ .../Extensions/KernelExtensionsTests.cs | 4 +- 13 files changed, 603 insertions(+), 10 deletions(-) create mode 100644 dotnet/samples/Concepts/AgentSyntax/Getting_Started/Step4_KernelFunctionStrategies.cs create mode 100644 dotnet/samples/Concepts/AgentSyntax/Getting_Started/Step5_JsonResult.cs create mode 100644 dotnet/samples/Concepts/AgentSyntax/RepoUtils/JsonResultTranslator.cs create mode 100644 dotnet/src/Agents/Core/Chat/KernelFunctionSelectionStrategy.cs create mode 100644 dotnet/src/Agents/Core/Chat/KernelFunctionTerminationStrategy.cs create mode 100644 dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionSelectionStrategyTests.cs create mode 100644 dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionTerminationStrategyTests.cs diff --git a/dotnet/samples/Concepts/AgentSyntax/AgentSyntax.csproj b/dotnet/samples/Concepts/AgentSyntax/AgentSyntax.csproj index 7f6111c23ef9..6d01d451fefe 100644 --- a/dotnet/samples/Concepts/AgentSyntax/AgentSyntax.csproj +++ b/dotnet/samples/Concepts/AgentSyntax/AgentSyntax.csproj @@ -10,7 +10,7 @@ true false - CS8618,IDE0009,CA1051,CA1050,CA1707,CA2007,VSTHRD111,CS1591,RCS1110,CA5394,SKEXP0001,SKEXP0010,SKEXP0110 + IDE0009,VSTHRD111,CS0612,CS1591,CS8618,CA1050,CA1051,CA1707,CA2007,CA5394,RCS1110,SKEXP0001,SKEXP0010,SKEXP0110 Library @@ -48,8 +48,4 @@ - - - - \ No newline at end of file diff --git a/dotnet/samples/Concepts/AgentSyntax/BaseTest.cs b/dotnet/samples/Concepts/AgentSyntax/BaseTest.cs index 96f967a55edc..3665af69382e 100644 --- a/dotnet/samples/Concepts/AgentSyntax/BaseTest.cs +++ b/dotnet/samples/Concepts/AgentSyntax/BaseTest.cs @@ -36,7 +36,7 @@ public abstract class BaseTest TestConfiguration.OpenAI.ChatModelId : TestConfiguration.AzureOpenAI.ChatDeploymentName; - protected Kernel CreateEmptyKernel() => Kernel.CreateBuilder().Build(); + protected Kernel CreateEmptyKernel() => new(); protected Kernel CreateKernelWithChatCompletion() { diff --git a/dotnet/samples/Concepts/AgentSyntax/Getting_Started/Step3_Chat.cs b/dotnet/samples/Concepts/AgentSyntax/Getting_Started/Step3_Chat.cs index 687f0101f473..db356f9a5135 100644 --- a/dotnet/samples/Concepts/AgentSyntax/Getting_Started/Step3_Chat.cs +++ b/dotnet/samples/Concepts/AgentSyntax/Getting_Started/Step3_Chat.cs @@ -24,7 +24,7 @@ public class Step3_Chat(ITestOutputHelper output) : BaseTest(output) private const string ReviewerInstructions = """ You are an art director who has opinions about copywriting born of a love for David Ogilvy. - The goal is to determine is the given copy is acceptable to print. + The goal is to determine if the given copy is acceptable to print. If so, state that it is approved. If not, provide insight on how to refine suggested copy without example. """; diff --git a/dotnet/samples/Concepts/AgentSyntax/Getting_Started/Step4_KernelFunctionStrategies.cs b/dotnet/samples/Concepts/AgentSyntax/Getting_Started/Step4_KernelFunctionStrategies.cs new file mode 100644 index 000000000000..5c26354c57e1 --- /dev/null +++ b/dotnet/samples/Concepts/AgentSyntax/Getting_Started/Step4_KernelFunctionStrategies.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Threading.Tasks; +using Examples; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Chat; +using Microsoft.SemanticKernel.ChatCompletion; +using Xunit; +using Xunit.Abstractions; + +namespace GettingStarted; + +/// +/// Demonstrate usage of and +/// to manage execution. +/// +public class Step4_KernelFunctionStrategies(ITestOutputHelper output) : BaseTest(output) +{ + private const string ReviewerName = "ArtDirector"; + private const string ReviewerInstructions = + """ + You are an art director who has opinions about copywriting born of a love for David Ogilvy. + The goal is to determine if the given copy is acceptable to print. + If so, state that it is approved. + If not, provide insight on how to refine suggested copy without examples. + """; + + private const string CopyWriterName = "Writer"; + private const string CopyWriterInstructions = + """ + You are a copywriter with ten years of experience and are known for brevity and a dry humor. + You're laser focused on the goal at hand. Don't waste time with chit chat. + The goal is to refine and decide on the single best copy as an expert in the field. + Consider suggestions when refining an idea. + """; + + [Fact] + public async Task RunAsync() + { + // Define the agents + ChatCompletionAgent agentReviewer = + new() + { + Instructions = ReviewerInstructions, + Name = ReviewerName, + Kernel = this.CreateKernelWithChatCompletion(), + }; + + ChatCompletionAgent agentWriter = + new() + { + Instructions = CopyWriterInstructions, + Name = CopyWriterName, + Kernel = this.CreateKernelWithChatCompletion(), + }; + + KernelFunction terminationFunction = + KernelFunctionFactory.CreateFromPrompt( + """ + Determine if the copy has been approved. If so, respond with a single word: yes + + History: + {{$history}} + """); + + KernelFunction selectionFunction = + KernelFunctionFactory.CreateFromPrompt( + """ + You are in a role playing game. + Carefully read the conversation history and carry on the conversation by specifying only the name of player to take the next turn. + + The available names are: + {{$agents}} + + History: + {{$history}} + """); + + // Create a chat for agent interaction. + AgentGroupChat chat = + new(agentWriter, agentReviewer) + { + ExecutionSettings = + new() + { + // Here KernelFunctionTerminationStrategy will terminate + // when the art-director has given their approval. + TerminationStrategy = + new KernelFunctionTerminationStrategy(terminationFunction, CreateKernelWithChatCompletion()) + { + // Only the art-director may approve. + Agents = [agentReviewer], + // Customer result parser to determine if the response is "yes" + ResultParser = (result) => result.GetValue()?.Contains("yes", StringComparison.OrdinalIgnoreCase) ?? false, + // The prompt variable name for the history argument. + HistoryVariableName = "history", + // Limit total number of turns + MaximumIterations = 10, + }, + // Here a KernelFunctionSelectionStrategy selects agents based on a prompt function. + SelectionStrategy = + new KernelFunctionSelectionStrategy(selectionFunction, CreateKernelWithChatCompletion()) + { + // Returns the entire result value as a string. + ResultParser = (result) => result.GetValue() ?? string.Empty, + // The prompt variable name for the agents argument. + AgentsVariableName = "agents", + // The prompt variable name for the history argument. + HistoryVariableName = "history", + }, + } + }; + + // Invoke chat and display messages. + string input = "concept: maps made out of egg cartons."; + chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); + this.WriteLine($"# {AuthorRole.User}: '{input}'"); + + await foreach (var content in chat.InvokeAsync()) + { + this.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + } + + this.WriteLine($"# IS COMPLETE: {chat.IsComplete}"); + } +} diff --git a/dotnet/samples/Concepts/AgentSyntax/Getting_Started/Step5_JsonResult.cs b/dotnet/samples/Concepts/AgentSyntax/Getting_Started/Step5_JsonResult.cs new file mode 100644 index 000000000000..acdcf90ba2f0 --- /dev/null +++ b/dotnet/samples/Concepts/AgentSyntax/Getting_Started/Step5_JsonResult.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Examples; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Chat; +using Microsoft.SemanticKernel.ChatCompletion; +using Resources; +using Xunit; +using Xunit.Abstractions; + +namespace GettingStarted; + +/// +/// Demonstrate parsing JSON response. +/// +public class Step5_JsonResult(ITestOutputHelper output) : BaseTest(output) +{ + private const string TutorName = "Tutor"; + private const string TutorInstructions = + """ + Think step-by-step and rate the user input on creativity and expressivness from 1-100. + + Respond in JSON format with the following JSON schema: + + { + "score": "integer (1-100)", + "notes": "the reason for your score" + } + """; + + [Fact] + public async Task RunAsync() + { + // Define the agents + ChatCompletionAgent agent = + new() + { + Instructions = TutorInstructions, + Name = TutorName, + Kernel = this.CreateKernelWithChatCompletion(), + }; + + // Create a chat for agent interaction. + AgentGroupChat chat = + new() + { + ExecutionSettings = + new() + { + // Here a TerminationStrategy subclass is used that will terminate when + // the response includes a score that is greater than or equal to 70. + TerminationStrategy = new ThresholdTerminationStrategy() + } + }; + + // Respond to user input + await InvokeAgentAsync("The sunset is very colorful."); + await InvokeAgentAsync("The sunset is setting over the mountains."); + await InvokeAgentAsync("The sunset is setting over the mountains and filled the sky with a deep red flame, setting the clouds ablaze."); + + // Local function to invoke agent and display the conversation messages. + async Task InvokeAgentAsync(string input) + { + chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); + + this.WriteLine($"# {AuthorRole.User}: '{input}'"); + + await foreach (var content in chat.InvokeAsync(agent)) + { + this.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + this.WriteLine($"# IS COMPLETE: {chat.IsComplete}"); + } + } + } + + private record struct InputScore(int score, string notes); + + private sealed class ThresholdTerminationStrategy : TerminationStrategy + { + protected override Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken) + { + string lastMessageContent = history[history.Count - 1].Content ?? string.Empty; + + InputScore? result = JsonResultTranslator.Translate(lastMessageContent); + + return Task.FromResult((result?.score ?? 0) >= 70); + } + } +} diff --git a/dotnet/samples/Concepts/AgentSyntax/RepoUtils/JsonResultTranslator.cs b/dotnet/samples/Concepts/AgentSyntax/RepoUtils/JsonResultTranslator.cs new file mode 100644 index 000000000000..66ab04c0769f --- /dev/null +++ b/dotnet/samples/Concepts/AgentSyntax/RepoUtils/JsonResultTranslator.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using Microsoft.SemanticKernel; + +namespace Resources; +/// +/// Supports parsing json from a text block that may contain literals delimiters: +/// +/// +/// +/// [json] +/// +/// +/// +/// +/// ``` +/// [json] +/// ``` +/// +/// +/// +/// +/// ```json +/// [json] +/// ``` +/// +/// +/// +/// +/// +/// Encountering json with this form of delimiters is not uncommon for agent scenarios. +/// +public static class JsonResultTranslator +{ + private const string LiteralDelimiter = "```"; + private const string JsonPrefix = "json"; + + /// + /// Utility method for extracting a JSON result from an agent response. + /// + /// A text result + /// The target type of the . + /// The JSON translated to the requested type. + public static TResult? Translate(string result) + { + string rawJson = ExtractJson(result); + + return JsonSerializer.Deserialize(rawJson); + } + + private static string ExtractJson(string result) + { + // Search for initial literal delimiter: ``` + int startIndex = result.IndexOf(LiteralDelimiter, System.StringComparison.Ordinal); + if (startIndex < 0) + { + // No initial delimiter, return entire expression. + return result; + } + + startIndex += LiteralDelimiter.Length; + + // Accommodate "json" prefix, if present. + if (JsonPrefix.Equals(result.Substring(startIndex, JsonPrefix.Length), System.StringComparison.OrdinalIgnoreCase)) + { + startIndex += JsonPrefix.Length; + } + + // Locate final literal delimiter + int endIndex = result.IndexOf(LiteralDelimiter, startIndex, System.StringComparison.OrdinalIgnoreCase); + if (endIndex < 0) + { + endIndex = result.Length; + } + + // Extract JSON + return result.Substring(startIndex, endIndex - startIndex); + } +} diff --git a/dotnet/src/Agents/Abstractions/KernelAgent.cs b/dotnet/src/Agents/Abstractions/KernelAgent.cs index 957510dc8649..061705670a2a 100644 --- a/dotnet/src/Agents/Abstractions/KernelAgent.cs +++ b/dotnet/src/Agents/Abstractions/KernelAgent.cs @@ -17,5 +17,5 @@ public abstract class KernelAgent : Agent /// /// Defaults to empty Kernel, but may be overridden. /// - public Kernel Kernel { get; init; } = Kernel.CreateBuilder().Build(); + public Kernel Kernel { get; init; } = new Kernel(); } diff --git a/dotnet/src/Agents/Core/Agents.Core.csproj b/dotnet/src/Agents/Core/Agents.Core.csproj index b1f5f593dd02..9fdf1fd90622 100644 --- a/dotnet/src/Agents/Core/Agents.Core.csproj +++ b/dotnet/src/Agents/Core/Agents.Core.csproj @@ -21,6 +21,7 @@ + diff --git a/dotnet/src/Agents/Core/Chat/KernelFunctionSelectionStrategy.cs b/dotnet/src/Agents/Core/Chat/KernelFunctionSelectionStrategy.cs new file mode 100644 index 000000000000..c11576b0ecbd --- /dev/null +++ b/dotnet/src/Agents/Core/Chat/KernelFunctionSelectionStrategy.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Agents.Chat; + +/// +/// Determines agent selection based on the evaluation of a . +/// +/// A used for selection criteria +/// A kernel instance with services for function execution. +public class KernelFunctionSelectionStrategy(KernelFunction function, Kernel kernel) : SelectionStrategy +{ + /// + /// The default value for . + /// + public const string DefaultAgentsVariableName = "_agents_"; + + /// + /// The default value for . + /// + public const string DefaultHistoryVariableName = "_history_"; + + /// + /// The key associated with the list of agent names when + /// invoking . + /// + public string AgentsVariableName { get; init; } = DefaultAgentsVariableName; + + /// + /// The key associated with the chat history when + /// invoking . + /// + public string HistoryVariableName { get; init; } = DefaultHistoryVariableName; + + /// + /// Optional arguments used when invoking . + /// + public KernelArguments? Arguments { get; init; } + + /// + /// The invoked as selection criteria. + /// + public KernelFunction Function { get; } = function; + + /// + /// The used when invoking . + /// + public Kernel Kernel => kernel; + + /// + /// A callback responsible for translating the + /// to the termination criteria. + /// + public Func ResultParser { get; init; } = (result) => result.GetValue() ?? string.Empty; + + /// + public sealed override async Task NextAsync(IReadOnlyList agents, IReadOnlyList history, CancellationToken cancellationToken = default) + { + KernelArguments originalArguments = this.Arguments ?? []; + KernelArguments arguments = + new(originalArguments, originalArguments.ExecutionSettings?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)) + { + { this.AgentsVariableName, string.Join(",", agents.Select(a => a.Name)) }, + { this.HistoryVariableName, JsonSerializer.Serialize(history) }, // TODO: GitHub Task #5894 + }; + + FunctionResult result = await this.Function.InvokeAsync(this.Kernel, arguments, cancellationToken).ConfigureAwait(false); + + string? agentName = this.ResultParser.Invoke(result); + if (string.IsNullOrEmpty(agentName)) + { + throw new KernelException("Agent Failure - Strategy unable to determine next agent."); + } + + return + agents.Where(a => (a.Name ?? a.Id) == agentName).FirstOrDefault() ?? + throw new KernelException($"Agent Failure - Strategy unable to select next agent: {agentName}"); + } +} diff --git a/dotnet/src/Agents/Core/Chat/KernelFunctionTerminationStrategy.cs b/dotnet/src/Agents/Core/Chat/KernelFunctionTerminationStrategy.cs new file mode 100644 index 000000000000..a2b8b7729198 --- /dev/null +++ b/dotnet/src/Agents/Core/Chat/KernelFunctionTerminationStrategy.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Agents.Chat; + +/// +/// Signals termination based on the evaluation of a . +/// +/// A used for termination criteria +/// A kernel instance with services for function execution. +public class KernelFunctionTerminationStrategy(KernelFunction function, Kernel kernel) : TerminationStrategy +{ + /// + /// The default value for . + /// + public const string DefaultAgentVariableName = "_agent_"; + + /// + /// The default value for . + /// + public const string DefaultHistoryVariableName = "_history_"; + + /// + /// The key associated with the agent name when + /// invoking . + /// + public string AgentVariableName { get; init; } = DefaultAgentVariableName; + + /// + /// The key associated with the chat history when + /// invoking . + /// + public string HistoryVariableName { get; init; } = DefaultHistoryVariableName; + + /// + /// Optional arguments used when invoking . + /// + public KernelArguments? Arguments { get; init; } + + /// + /// The invoked as termination criteria. + /// + public KernelFunction Function { get; } = function; + + /// + /// The used when invoking . + /// + public Kernel Kernel => kernel; + + /// + /// A callback responsible for translating the + /// to the termination criteria. + /// + public Func ResultParser { get; init; } = (_) => true; + + /// + protected sealed override async Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken = default) + { + KernelArguments originalArguments = this.Arguments ?? []; + KernelArguments arguments = + new(originalArguments, originalArguments.ExecutionSettings?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)) + { + { this.AgentVariableName, agent.Name ?? agent.Id }, + { this.HistoryVariableName, JsonSerializer.Serialize(history) }, // TODO: GitHub Task #5894 + }; + + FunctionResult result = await this.Function.InvokeAsync(this.Kernel, arguments, cancellationToken).ConfigureAwait(false); + + return this.ResultParser.Invoke(result); + } +} diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionSelectionStrategyTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionSelectionStrategyTests.cs new file mode 100644 index 000000000000..af045e67873d --- /dev/null +++ b/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionSelectionStrategyTests.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Linq; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Chat; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Moq; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.Core.Chat; + +/// +/// Unit testing of . +/// +public class KernelFunctionSelectionStrategyTests +{ + /// + /// Verify default state and behavior + /// + [Fact] + public async Task VerifyKernelFunctionSelectionStrategyDefaultsAsync() + { + Mock mockAgent = new(); + KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin(mockAgent.Object.Id)); + + KernelFunctionSelectionStrategy strategy = + new(plugin.Single(), new()) + { + ResultParser = (result) => result.GetValue() ?? string.Empty, + }; + + Assert.Null(strategy.Arguments); + Assert.NotNull(strategy.Kernel); + Assert.NotNull(strategy.ResultParser); + + Agent nextAgent = await strategy.NextAsync([mockAgent.Object], []); + + Assert.NotNull(nextAgent); + Assert.Equal(mockAgent.Object, nextAgent); + } + + /// + /// Verify strategy mismatch. + /// + [Fact] + public async Task VerifyKernelFunctionSelectionStrategyParsingAsync() + { + Mock mockAgent = new(); + KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin(string.Empty)); + + KernelFunctionSelectionStrategy strategy = + new(plugin.Single(), new()) + { + Arguments = new(new OpenAIPromptExecutionSettings()) { { "key", mockAgent.Object.Name } }, + ResultParser = (result) => result.GetValue() ?? string.Empty, + }; + + await Assert.ThrowsAsync(() => strategy.NextAsync([mockAgent.Object], [])); + } + + private sealed class TestPlugin(string agentName) + { + [KernelFunction] + public string GetValue() => agentName; + } +} diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionTerminationStrategyTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionTerminationStrategyTests.cs new file mode 100644 index 000000000000..6f0b446e5e7a --- /dev/null +++ b/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionTerminationStrategyTests.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Chat; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Moq; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.Core.Chat; + +/// +/// Unit testing of . +/// +public class KernelFunctionTerminationStrategyTests +{ + /// + /// Verify default state and behavior + /// + [Fact] + public async Task VerifyKernelFunctionTerminationStrategyDefaultsAsync() + { + KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin()); + + KernelFunctionTerminationStrategy strategy = new(plugin.Single(), new()); + + Assert.Null(strategy.Arguments); + Assert.NotNull(strategy.Kernel); + Assert.NotNull(strategy.ResultParser); + + Mock mockAgent = new(); + + bool isTerminating = await strategy.ShouldTerminateAsync(mockAgent.Object, []); + + Assert.True(isTerminating); + } + + /// + /// Verify strategy with result parser. + /// + [Fact] + public async Task VerifyKernelFunctionTerminationStrategyParsingAsync() + { + KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin()); + + KernelFunctionTerminationStrategy strategy = + new(plugin.Single(), new()) + { + Arguments = new(new OpenAIPromptExecutionSettings()) { { "key", "test" } }, + ResultParser = (result) => string.Equals("test", result.GetValue(), StringComparison.OrdinalIgnoreCase) + }; + + Mock mockAgent = new(); + + bool isTerminating = await strategy.ShouldTerminateAsync(mockAgent.Object, []); + + Assert.True(isTerminating); + } + + private sealed class TestPlugin() + { + [KernelFunction] + public string GetValue(KernelArguments? arguments) + { + string? argument = arguments?.First().Value?.ToString(); + return argument ?? string.Empty; + } + } +} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelExtensionsTests.cs index 8e52cc171e9a..3f982f3a7b47 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelExtensionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelExtensionsTests.cs @@ -17,7 +17,7 @@ public class KernelExtensionsTests [Fact] public void VerifyGetKernelFunctionLookup() { - Kernel kernel = Kernel.CreateBuilder().Build(); + Kernel kernel = new(); KernelPlugin plugin = KernelPluginFactory.CreateFromType(); kernel.Plugins.Add(plugin); @@ -32,7 +32,7 @@ public void VerifyGetKernelFunctionLookup() [Fact] public void VerifyGetKernelFunctionInvalid() { - Kernel kernel = Kernel.CreateBuilder().Build(); + Kernel kernel = new(); KernelPlugin plugin = KernelPluginFactory.CreateFromType(); kernel.Plugins.Add(plugin);